Skip to main content

rustlavel_view/
value.rs

1//! The view's rules for reading a `Json` value: truth, order, and text.
2//!
3//! There is deliberately no second value type here. Templates render whatever
4//! the rest of the framework already speaks — `rustlavel_core::Json` — so a
5//! query result or an API payload can go straight into a view without a
6//! conversion layer in between. What lives in this module is only the handful
7//! of decisions a template engine has to make on top of that model.
8
9use rustlavel_core::Json;
10use std::cmp::Ordering;
11
12/// Whether a value counts as true for `@if` and `&&`/`||`.
13///
14/// The rules follow PHP closely enough that a Laravel developer's instinct is
15/// right: nothing, zero, empty text, and an empty collection are all falsy.
16/// That is what makes `@if(items)` read naturally instead of forcing
17/// `@if(items.length > 0)`.
18pub fn truthy(value: &Json) -> bool {
19    match value {
20        Json::Null => false,
21        Json::Bool(flag) => *flag,
22        // NaN is falsy for the same reason it is not equal to itself: no
23        // comparison involving it should quietly succeed.
24        Json::Number(number) => *number != 0.0 && !number.is_nan(),
25        Json::String(text) => !text.is_empty(),
26        Json::Array(items) => !items.is_empty(),
27        Json::Object(map) => !map.is_empty(),
28    }
29}
30
31/// Ordering for `<`, `<=`, `>` and `>=`.
32///
33/// Only numbers and strings are ordered, and only against their own kind:
34/// comparing a number to a string is a mistake in the view, and answering
35/// `false` to both `a < b` and `a > b` is a far better clue than inventing a
36/// coercion rule the author has to memorise.
37pub fn compare(left: &Json, right: &Json) -> Option<Ordering> {
38    match (left, right) {
39        (Json::Number(a), Json::Number(b)) => a.partial_cmp(b),
40        (Json::String(a), Json::String(b)) => Some(a.cmp(b)),
41        _ => None,
42    }
43}
44
45/// Render a value as the text `{{ }}` would emit, before escaping.
46///
47/// `null` becomes the empty string. That is the single most important line in
48/// this file: a missing key must leave a hole in the page, never take the page
49/// down.
50pub fn to_text(value: &Json) -> String {
51    match value {
52        Json::Null => String::new(),
53        Json::Bool(flag) => flag.to_string(),
54        Json::String(text) => text.clone(),
55        // Ids are the most common numeric output, so integral values render
56        // without the `.0` that `f64` would otherwise insist on.
57        Json::Number(number) if number.fract() == 0.0 && number.abs() < 1e15 => {
58            (*number as i64).to_string()
59        }
60        Json::Number(number) => number.to_string(),
61        // Arrays and objects have no obvious text form; compact JSON is at
62        // least honest about what the author asked for.
63        other => other.to_string(),
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn emptiness_of_every_kind_is_falsy() {
73        assert!(!truthy(&Json::Null));
74        assert!(!truthy(&Json::Bool(false)));
75        assert!(!truthy(&Json::Number(0.0)));
76        assert!(!truthy(&Json::from("")));
77        assert!(!truthy(&Json::Array(Vec::new())));
78        assert!(!truthy(&Json::object(Vec::<(String, Json)>::new())));
79
80        assert!(truthy(&Json::Bool(true)));
81        assert!(truthy(&Json::Number(-1.0)));
82        assert!(truthy(&Json::from("0")));
83        assert!(truthy(&Json::from(vec![1])));
84    }
85
86    #[test]
87    fn only_matching_kinds_are_ordered() {
88        assert_eq!(compare(&Json::from(1), &Json::from(2)), Some(Ordering::Less));
89        assert_eq!(compare(&Json::from("a"), &Json::from("b")), Some(Ordering::Less));
90        assert_eq!(compare(&Json::from(1), &Json::from("1")), None);
91    }
92
93    #[test]
94    fn null_renders_as_nothing_and_integers_keep_their_shape() {
95        assert_eq!(to_text(&Json::Null), "");
96        assert_eq!(to_text(&Json::from(42)), "42");
97        assert_eq!(to_text(&Json::from(1.5)), "1.5");
98        assert_eq!(to_text(&Json::Bool(true)), "true");
99        assert_eq!(to_text(&Json::from(vec![1, 2])), "[1,2]");
100    }
101}