liquid_value/
scalar.rs

1use std::borrow;
2use std::cmp::Ordering;
3use std::fmt;
4
5use chrono;
6
7/// Liquid's native date/time type.
8pub type Date = chrono::DateTime<chrono::FixedOffset>;
9
10/// A Liquid scalar value
11#[derive(Clone, Debug, Serialize, Deserialize)]
12pub struct ScalarCow<'s>(ScalarCowEnum<'s>);
13
14/// A Liquid scalar value
15pub type Scalar = ScalarCow<'static>;
16
17/// An enum to represent different value types
18#[derive(Clone, Debug, Serialize, Deserialize)]
19#[serde(untagged)]
20enum ScalarCowEnum<'s> {
21    Integer(i32),
22    Float(f64),
23    Bool(bool),
24    #[serde(with = "friendly_date")]
25    Date(Date),
26    Str(borrow::Cow<'s, str>),
27}
28
29impl<'s> ScalarCow<'s> {
30    /// Convert a value into a `ScalarCow`.
31    pub fn new<T: Into<Self>>(value: T) -> Self {
32        value.into()
33    }
34
35    /// A `Display` for a `Scalar` as source code.
36    pub fn source(&self) -> ScalarSource<'_> {
37        ScalarSource(&self.0)
38    }
39
40    /// A `Display` for a `Scalar` rendered for the user.
41    pub fn render(&self) -> ScalarRendered<'_> {
42        ScalarRendered(&self.0)
43    }
44
45    /// Create an owned version of the value.
46    pub fn into_owned(self) -> Self {
47        match self.0 {
48            ScalarCowEnum::Str(x) => Scalar::new(x.into_owned()),
49            _ => self,
50        }
51    }
52
53    /// Create a reference to the value.
54    pub fn as_ref<'r: 's>(&'r self) -> ScalarCow<'r> {
55        match self.0 {
56            ScalarCowEnum::Integer(x) => ScalarCow::new(x),
57            ScalarCowEnum::Float(x) => ScalarCow::new(x),
58            ScalarCowEnum::Bool(x) => ScalarCow::new(x),
59            ScalarCowEnum::Date(x) => ScalarCow::new(x),
60            ScalarCowEnum::Str(ref x) => ScalarCow::new(x.as_ref()),
61        }
62    }
63
64    /// Interpret as a string.
65    pub fn to_str(&self) -> borrow::Cow<'_, str> {
66        match self.0 {
67            ScalarCowEnum::Integer(ref x) => borrow::Cow::Owned(x.to_string()),
68            ScalarCowEnum::Float(ref x) => borrow::Cow::Owned(x.to_string()),
69            ScalarCowEnum::Bool(ref x) => borrow::Cow::Owned(x.to_string()),
70            ScalarCowEnum::Date(ref x) => borrow::Cow::Owned(x.format(DATE_FORMAT).to_string()),
71            ScalarCowEnum::Str(ref x) => borrow::Cow::Borrowed(x.as_ref()),
72        }
73    }
74
75    /// Convert to a string.
76    pub fn into_string(self) -> String {
77        match self.0 {
78            ScalarCowEnum::Integer(x) => x.to_string(),
79            ScalarCowEnum::Float(x) => x.to_string(),
80            ScalarCowEnum::Bool(x) => x.to_string(),
81            ScalarCowEnum::Date(x) => x.to_string(),
82            ScalarCowEnum::Str(x) => x.into_owned(),
83        }
84    }
85
86    /// Interpret as an integer, if possible
87    pub fn to_integer(&self) -> Option<i32> {
88        match self.0 {
89            ScalarCowEnum::Integer(ref x) => Some(*x),
90            ScalarCowEnum::Str(ref x) => x.parse::<i32>().ok(),
91            _ => None,
92        }
93    }
94
95    /// Interpret as a float, if possible
96    pub fn to_float(&self) -> Option<f64> {
97        match self.0 {
98            ScalarCowEnum::Integer(ref x) => Some(f64::from(*x)),
99            ScalarCowEnum::Float(ref x) => Some(*x),
100            ScalarCowEnum::Str(ref x) => x.parse::<f64>().ok(),
101            _ => None,
102        }
103    }
104
105    /// Interpret as a bool, if possible
106    pub fn to_bool(&self) -> Option<bool> {
107        match self.0 {
108            ScalarCowEnum::Bool(ref x) => Some(*x),
109            _ => None,
110        }
111    }
112
113    /// Interpret as a date, if possible
114    pub fn to_date(&self) -> Option<Date> {
115        match self.0 {
116            ScalarCowEnum::Date(ref x) => Some(*x),
117            ScalarCowEnum::Str(ref x) => parse_date(x.as_ref()),
118            _ => None,
119        }
120    }
121
122    /// Evaluate using Liquid "truthiness"
123    pub fn is_truthy(&self) -> bool {
124        // encode Ruby truthiness: all values except false and nil are true
125        match self.0 {
126            ScalarCowEnum::Bool(ref x) => *x,
127            _ => true,
128        }
129    }
130
131    /// Whether a default constructed value.
132    pub fn is_default(&self) -> bool {
133        // encode Ruby truthiness: all values except false and nil are true
134        match self.0 {
135            ScalarCowEnum::Bool(ref x) => !*x,
136            ScalarCowEnum::Str(ref x) => x.is_empty(),
137            _ => false,
138        }
139    }
140
141    /// Report the data type (generally for error reporting).
142    pub fn type_name(&self) -> &'static str {
143        match self.0 {
144            ScalarCowEnum::Integer(_) => "whole number",
145            ScalarCowEnum::Float(_) => "fractional number",
146            ScalarCowEnum::Bool(_) => "boolean",
147            ScalarCowEnum::Date(_) => "date",
148            ScalarCowEnum::Str(_) => "string",
149        }
150    }
151}
152
153impl<'s> From<i32> for ScalarCow<'s> {
154    fn from(s: i32) -> Self {
155        ScalarCow {
156            0: ScalarCowEnum::Integer(s),
157        }
158    }
159}
160
161impl<'s> From<f64> for ScalarCow<'s> {
162    fn from(s: f64) -> Self {
163        ScalarCow {
164            0: ScalarCowEnum::Float(s),
165        }
166    }
167}
168
169impl<'s> From<bool> for ScalarCow<'s> {
170    fn from(s: bool) -> Self {
171        ScalarCow {
172            0: ScalarCowEnum::Bool(s),
173        }
174    }
175}
176
177impl<'s> From<Date> for ScalarCow<'s> {
178    fn from(s: Date) -> Self {
179        ScalarCow {
180            0: ScalarCowEnum::Date(s),
181        }
182    }
183}
184
185impl<'s> From<String> for ScalarCow<'s> {
186    fn from(s: String) -> Self {
187        ScalarCow {
188            0: ScalarCowEnum::Str(s.into()),
189        }
190    }
191}
192
193impl<'s> From<&'s String> for ScalarCow<'s> {
194    fn from(s: &'s String) -> ScalarCow<'s> {
195        ScalarCow {
196            0: ScalarCowEnum::Str(s.as_str().into()),
197        }
198    }
199}
200
201impl<'s> From<&'s str> for ScalarCow<'s> {
202    fn from(s: &'s str) -> Self {
203        ScalarCow {
204            0: ScalarCowEnum::Str(s.into()),
205        }
206    }
207}
208
209impl<'s> From<borrow::Cow<'s, str>> for ScalarCow<'s> {
210    fn from(s: borrow::Cow<'s, str>) -> Self {
211        ScalarCow {
212            0: ScalarCowEnum::Str(s),
213        }
214    }
215}
216
217impl<'s> PartialEq<ScalarCow<'s>> for ScalarCow<'s> {
218    fn eq(&self, other: &Self) -> bool {
219        scalar_eq(self, other)
220    }
221}
222
223impl<'s> PartialEq<i32> for ScalarCow<'s> {
224    fn eq(&self, other: &i32) -> bool {
225        let other = (*other).into();
226        scalar_eq(self, &other)
227    }
228}
229
230impl<'s> PartialEq<f64> for ScalarCow<'s> {
231    fn eq(&self, other: &f64) -> bool {
232        let other = (*other).into();
233        scalar_eq(self, &other)
234    }
235}
236
237impl<'s> PartialEq<bool> for ScalarCow<'s> {
238    fn eq(&self, other: &bool) -> bool {
239        let other = (*other).into();
240        scalar_eq(self, &other)
241    }
242}
243
244impl<'s> PartialEq<Date> for ScalarCow<'s> {
245    fn eq(&self, other: &Date) -> bool {
246        let other = (*other).into();
247        scalar_eq(self, &other)
248    }
249}
250
251impl<'s> PartialEq<str> for ScalarCow<'s> {
252    fn eq(&self, other: &str) -> bool {
253        let other = other.into();
254        scalar_eq(self, &other)
255    }
256}
257
258impl<'s> Eq for ScalarCow<'s> {}
259
260impl<'s> PartialOrd<ScalarCow<'s>> for ScalarCow<'s> {
261    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
262        scalar_cmp(self, other)
263    }
264}
265
266impl<'s> PartialOrd<i32> for ScalarCow<'s> {
267    fn partial_cmp(&self, other: &i32) -> Option<Ordering> {
268        let other = (*other).into();
269        scalar_cmp(self, &other)
270    }
271}
272
273impl<'s> PartialOrd<f64> for ScalarCow<'s> {
274    fn partial_cmp(&self, other: &f64) -> Option<Ordering> {
275        let other = (*other).into();
276        scalar_cmp(self, &other)
277    }
278}
279
280impl<'s> PartialOrd<bool> for ScalarCow<'s> {
281    fn partial_cmp(&self, other: &bool) -> Option<Ordering> {
282        let other = (*other).into();
283        scalar_cmp(self, &other)
284    }
285}
286
287impl<'s> PartialOrd<Date> for ScalarCow<'s> {
288    fn partial_cmp(&self, other: &Date) -> Option<Ordering> {
289        let other = (*other).into();
290        scalar_cmp(self, &other)
291    }
292}
293
294impl<'s> PartialOrd<str> for ScalarCow<'s> {
295    fn partial_cmp(&self, other: &str) -> Option<Ordering> {
296        let other = other.into();
297        scalar_cmp(self, &other)
298    }
299}
300
301/// A `Display` for a `Scalar` as source code.
302#[derive(Debug)]
303pub struct ScalarSource<'s>(&'s ScalarCowEnum<'s>);
304
305impl<'s> fmt::Display for ScalarSource<'s> {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        match self.0 {
308            ScalarCowEnum::Integer(ref x) => write!(f, "{}", x),
309            ScalarCowEnum::Float(ref x) => write!(f, "{}", x),
310            ScalarCowEnum::Bool(ref x) => write!(f, "{}", x),
311            ScalarCowEnum::Date(ref x) => write!(f, "{}", x.format(DATE_FORMAT)),
312            ScalarCowEnum::Str(ref x) => write!(f, r#""{}""#, x),
313        }
314    }
315}
316
317/// A `Display` for a `Scalar` rendered for the user.
318#[derive(Debug)]
319pub struct ScalarRendered<'s>(&'s ScalarCowEnum<'s>);
320
321impl<'s> fmt::Display for ScalarRendered<'s> {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        // Must match `ScalarCow::to_str`
324        match self.0 {
325            ScalarCowEnum::Integer(ref x) => write!(f, "{}", x),
326            ScalarCowEnum::Float(ref x) => write!(f, "{}", x),
327            ScalarCowEnum::Bool(ref x) => write!(f, "{}", x),
328            ScalarCowEnum::Date(ref x) => write!(f, "{}", x.format(DATE_FORMAT)),
329            ScalarCowEnum::Str(ref x) => write!(f, "{}", x),
330        }
331    }
332}
333
334fn scalar_eq<'s>(lhs: &ScalarCow<'s>, rhs: &ScalarCow<'s>) -> bool {
335    match (&lhs.0, &rhs.0) {
336        (&ScalarCowEnum::Integer(x), &ScalarCowEnum::Integer(y)) => x == y,
337        (&ScalarCowEnum::Integer(x), &ScalarCowEnum::Float(y)) => (f64::from(x)) == y,
338        (&ScalarCowEnum::Float(x), &ScalarCowEnum::Integer(y)) => x == (f64::from(y)),
339        (&ScalarCowEnum::Float(x), &ScalarCowEnum::Float(y)) => x == y,
340        (&ScalarCowEnum::Bool(x), &ScalarCowEnum::Bool(y)) => x == y,
341        (&ScalarCowEnum::Date(x), &ScalarCowEnum::Date(y)) => x == y,
342        (&ScalarCowEnum::Str(ref x), &ScalarCowEnum::Str(ref y)) => x == y,
343        // encode Ruby truthiness: all values except false and nil are true
344        (_, &ScalarCowEnum::Bool(b)) | (&ScalarCowEnum::Bool(b), _) => b,
345        _ => false,
346    }
347}
348
349fn scalar_cmp<'s>(lhs: &ScalarCow<'s>, rhs: &ScalarCow<'s>) -> Option<Ordering> {
350    match (&lhs.0, &rhs.0) {
351        (&ScalarCowEnum::Integer(x), &ScalarCowEnum::Integer(y)) => x.partial_cmp(&y),
352        (&ScalarCowEnum::Integer(x), &ScalarCowEnum::Float(y)) => (f64::from(x)).partial_cmp(&y),
353        (&ScalarCowEnum::Float(x), &ScalarCowEnum::Integer(y)) => x.partial_cmp(&(f64::from(y))),
354        (&ScalarCowEnum::Float(x), &ScalarCowEnum::Float(y)) => x.partial_cmp(&y),
355        (&ScalarCowEnum::Bool(x), &ScalarCowEnum::Bool(y)) => x.partial_cmp(&y),
356        (&ScalarCowEnum::Date(x), &ScalarCowEnum::Date(y)) => x.partial_cmp(&y),
357        (&ScalarCowEnum::Str(ref x), &ScalarCowEnum::Str(ref y)) => x.partial_cmp(y),
358        _ => None,
359    }
360}
361
362const DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S %z";
363
364mod friendly_date {
365    use super::*;
366    use serde::{self, Deserialize, Deserializer, Serializer};
367
368    pub(crate) fn serialize<S>(date: &Date, serializer: S) -> Result<S::Ok, S::Error>
369    where
370        S: Serializer,
371    {
372        let s = date.format(DATE_FORMAT).to_string();
373        serializer.serialize_str(&s)
374    }
375
376    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Date, D::Error>
377    where
378        D: Deserializer<'de>,
379    {
380        let s = String::deserialize(deserializer)?;
381        Date::parse_from_str(&s, DATE_FORMAT).map_err(serde::de::Error::custom)
382    }
383}
384
385fn parse_date(s: &str) -> Option<Date> {
386    match s {
387        "now" | "today" => {
388            let now = chrono::offset::Utc::now();
389            let now = now.naive_utc();
390            let now = chrono::DateTime::from_utc(now, chrono::offset::FixedOffset::east(0));
391            Some(now)
392        }
393        _ => {
394            let formats = ["%d %B %Y %H:%M:%S %z", "%Y-%m-%d %H:%M:%S %z"];
395            formats
396                .iter()
397                .filter_map(|f| Date::parse_from_str(s, f).ok())
398                .next()
399        }
400    }
401}
402
403#[cfg(test)]
404mod test {
405    use super::*;
406
407    static TRUE: ScalarCow<'_> = ScalarCow(ScalarCowEnum::Bool(true));
408    static FALSE: ScalarCow<'_> = ScalarCow(ScalarCowEnum::Bool(false));
409
410    #[test]
411    fn test_to_str_bool() {
412        assert_eq!(TRUE.to_str(), "true");
413    }
414
415    #[test]
416    fn test_to_str_integer() {
417        let val: ScalarCow<'_> = 42i32.into();
418        assert_eq!(val.to_str(), "42");
419    }
420
421    #[test]
422    fn test_to_str_float() {
423        let val: ScalarCow<'_> = 42f64.into();
424        assert_eq!(val.to_str(), "42");
425
426        let val: ScalarCow<'_> = 42.34.into();
427        assert_eq!(val.to_str(), "42.34");
428    }
429
430    #[test]
431    fn test_to_str_str() {
432        let val: ScalarCow<'_> = "foobar".into();
433        assert_eq!(val.to_str(), "foobar");
434    }
435
436    #[test]
437    fn test_to_integer_bool() {
438        assert_eq!(TRUE.to_integer(), None);
439    }
440
441    #[test]
442    fn test_to_integer_integer() {
443        let val: ScalarCow<'_> = 42i32.into();
444        assert_eq!(val.to_integer(), Some(42i32));
445    }
446
447    #[test]
448    fn test_to_integer_float() {
449        let val: ScalarCow<'_> = 42f64.into();
450        assert_eq!(val.to_integer(), None);
451
452        let val: ScalarCow<'_> = 42.34.into();
453        assert_eq!(val.to_integer(), None);
454    }
455
456    #[test]
457    fn test_to_integer_str() {
458        let val: ScalarCow<'_> = "foobar".into();
459        assert_eq!(val.to_integer(), None);
460
461        let val: ScalarCow<'_> = "42.34".into();
462        assert_eq!(val.to_integer(), None);
463
464        let val: ScalarCow<'_> = "42".into();
465        assert_eq!(val.to_integer(), Some(42));
466    }
467
468    #[test]
469    fn test_to_float_bool() {
470        assert_eq!(TRUE.to_float(), None);
471    }
472
473    #[test]
474    fn test_to_float_integer() {
475        let val: ScalarCow<'_> = 42i32.into();
476        assert_eq!(val.to_float(), Some(42f64));
477    }
478
479    #[test]
480    fn test_to_float_float() {
481        let val: ScalarCow<'_> = 42f64.into();
482        assert_eq!(val.to_float(), Some(42f64));
483
484        let val: ScalarCow<'_> = 42.34.into();
485        assert_eq!(val.to_float(), Some(42.34));
486    }
487
488    #[test]
489    fn test_to_float_str() {
490        let val: ScalarCow<'_> = "foobar".into();
491        assert_eq!(val.to_float(), None);
492
493        let val: ScalarCow<'_> = "42.34".into();
494        assert_eq!(val.to_float(), Some(42.34));
495
496        let val: ScalarCow<'_> = "42".into();
497        assert_eq!(val.to_float(), Some(42f64));
498    }
499
500    #[test]
501    fn test_to_bool_bool() {
502        assert_eq!(TRUE.to_bool(), Some(true));
503    }
504
505    #[test]
506    fn test_to_bool_integer() {
507        let val: ScalarCow<'_> = 42i32.into();
508        assert_eq!(val.to_bool(), None);
509    }
510
511    #[test]
512    fn test_to_bool_float() {
513        let val: ScalarCow<'_> = 42f64.into();
514        assert_eq!(val.to_bool(), None);
515
516        let val: ScalarCow<'_> = 42.34.into();
517        assert_eq!(val.to_bool(), None);
518    }
519
520    #[test]
521    fn test_to_bool_str() {
522        let val: ScalarCow<'_> = "foobar".into();
523        assert_eq!(val.to_bool(), None);
524
525        let val: ScalarCow<'_> = "true".into();
526        assert_eq!(val.to_bool(), None);
527
528        let val: ScalarCow<'_> = "false".into();
529        assert_eq!(val.to_bool(), None);
530    }
531
532    #[test]
533    fn integer_equality() {
534        let val: ScalarCow<'_> = 42i32.into();
535        let zero: ScalarCow<'_> = 0i32.into();
536        assert_eq!(val, val);
537        assert_eq!(zero, zero);
538        assert!(val != zero);
539        assert!(zero != val);
540    }
541
542    #[test]
543    fn integers_have_ruby_truthiness() {
544        let val: ScalarCow<'_> = 42i32.into();
545        let zero: ScalarCow<'_> = 0i32.into();
546        assert_eq!(TRUE, val);
547        assert_eq!(val, TRUE);
548        assert!(val.is_truthy());
549
550        assert_eq!(TRUE, zero);
551        assert_eq!(zero, TRUE);
552        assert!(zero.is_truthy());
553    }
554
555    #[test]
556    fn float_equality() {
557        let val: ScalarCow<'_> = 42f64.into();
558        let zero: ScalarCow<'_> = 0f64.into();
559        assert_eq!(val, val);
560        assert_eq!(zero, zero);
561        assert!(val != zero);
562        assert!(zero != val);
563    }
564
565    #[test]
566    fn floats_have_ruby_truthiness() {
567        let val: ScalarCow<'_> = 42f64.into();
568        let zero: ScalarCow<'_> = 0f64.into();
569        assert_eq!(TRUE, val);
570        assert_eq!(val, TRUE);
571        assert!(val.is_truthy());
572
573        assert_eq!(TRUE, zero);
574        assert_eq!(zero, TRUE);
575        assert!(zero.is_truthy());
576    }
577
578    #[test]
579    fn boolean_equality() {
580        assert_eq!(TRUE, TRUE);
581        assert_eq!(FALSE, FALSE);
582        assert!(FALSE != TRUE);
583        assert!(TRUE != FALSE);
584    }
585
586    #[test]
587    fn booleans_have_ruby_truthiness() {
588        assert!(TRUE.is_truthy());
589        assert!(!FALSE.is_truthy());
590    }
591
592    #[test]
593    fn string_equality() {
594        let alpha: ScalarCow<'_> = "alpha".into();
595        let beta: ScalarCow<'_> = "beta".into();
596        let empty: ScalarCow<'_> = "".into();
597        assert_eq!(alpha, alpha);
598        assert_eq!(empty, empty);
599        assert!(alpha != beta);
600        assert!(beta != alpha);
601    }
602
603    #[test]
604    fn strings_have_ruby_truthiness() {
605        // all strings in ruby are true
606        let alpha: ScalarCow<'_> = "alpha".into();
607        let empty: ScalarCow<'_> = "".into();
608        assert_eq!(TRUE, alpha);
609        assert_eq!(alpha, TRUE);
610        assert!(alpha.is_truthy());
611
612        assert_eq!(TRUE, empty);
613        assert_eq!(empty, TRUE);
614        assert!(empty.is_truthy());
615    }
616
617    #[test]
618    fn parse_date_empty_is_bad() {
619        assert!(parse_date("").is_none());
620    }
621
622    #[test]
623    fn parse_date_bad() {
624        assert!(parse_date("aaaaa").is_none());
625    }
626
627    #[test]
628    fn parse_date_now() {
629        assert!(parse_date("now").is_some());
630    }
631
632    #[test]
633    fn parse_date_today() {
634        assert!(parse_date("today").is_some());
635    }
636}