handlebars/helpers/
helper_extras.rs

1//! Helpers for boolean operations
2
3use std::cmp::Ordering;
4use std::str::FromStr;
5
6use num_order::NumOrd;
7use serde_json::Value as Json;
8
9use crate::json::value::JsonTruthy;
10
11handlebars_helper!(eq: |x: Json, y: Json| x == y);
12handlebars_helper!(ne: |x: Json, y: Json| x != y);
13handlebars_helper!(gt: |x: Json, y: Json| compare_json(x, y) == Some(Ordering::Greater));
14handlebars_helper!(gte: |x: Json, y: Json| compare_json(x, y).is_some_and(|ord| ord != Ordering::Less));
15handlebars_helper!(lt: |x: Json, y: Json| compare_json(x, y) == Some(Ordering::Less));
16handlebars_helper!(lte: |x: Json, y: Json| compare_json(x, y).is_some_and(|ord| ord != Ordering::Greater));
17handlebars_helper!(not: |x: Json| !x.is_truthy(false));
18handlebars_helper!(len: |x: Json| {
19    match x {
20        Json::Array(a) => a.len(),
21        Json::Object(m) => m.len(),
22        Json::String(s) => s.len(),
23        _ => 0
24    }
25});
26
27fn compare_json(x: &Json, y: &Json) -> Option<Ordering> {
28    fn cmp_num_str(a_num: &serde_json::Number, b_str: &str) -> Option<Ordering> {
29        let b_num = serde_json::Number::from_str(b_str).ok()?;
30        cmp_nums(a_num, &b_num)
31    }
32
33    // this function relies on serde_json::Numbers coerce logic
34    // for number value between [0, u64::MAX], is_u64() returns true
35    // for number value between [i64::MIN, i64::MAX], is_i64() returns true
36    // for others, is_f64() returns true, note that this behaviour is not
37    //  guaranteed according to serde_json docs
38    fn cmp_nums(a_num: &serde_json::Number, b_num: &serde_json::Number) -> Option<Ordering> {
39        if a_num.is_u64() {
40            let a = a_num.as_u64()?;
41            if b_num.is_u64() {
42                NumOrd::num_partial_cmp(&a, &b_num.as_u64()?)
43            } else if b_num.is_i64() {
44                NumOrd::num_partial_cmp(&a, &b_num.as_i64()?)
45            } else {
46                NumOrd::num_partial_cmp(&a, &b_num.as_f64()?)
47            }
48        } else if a_num.is_i64() {
49            let a = a_num.as_i64()?;
50            if b_num.is_u64() {
51                NumOrd::num_partial_cmp(&a, &b_num.as_u64()?)
52            } else if b_num.is_i64() {
53                NumOrd::num_partial_cmp(&a, &b_num.as_i64()?)
54            } else {
55                NumOrd::num_partial_cmp(&a, &b_num.as_f64()?)
56            }
57        } else {
58            let a = a_num.as_f64()?;
59            if b_num.is_u64() {
60                NumOrd::num_partial_cmp(&a, &b_num.as_u64()?)
61            } else if b_num.is_i64() {
62                NumOrd::num_partial_cmp(&a, &b_num.as_i64()?)
63            } else {
64                NumOrd::num_partial_cmp(&a, &b_num.as_f64()?)
65            }
66        }
67    }
68
69    match (x, y) {
70        (Json::Number(a), Json::Number(b)) => cmp_nums(a, b),
71        (Json::String(a), Json::String(b)) => Some(a.cmp(b)),
72        (Json::Bool(a), Json::Bool(b)) => Some(a.cmp(b)),
73        (Json::Number(a), Json::String(b)) => cmp_num_str(a, b),
74        (Json::String(a), Json::Number(b)) => cmp_num_str(b, a).map(Ordering::reverse),
75        _ => None,
76    }
77}
78
79#[allow(non_camel_case_types)]
80pub struct and;
81
82impl crate::HelperDef for and {
83    fn call_inner<'reg: 'rc, 'rc>(
84        &self,
85        h: &crate::Helper<'rc>,
86        _r: &'reg crate::Handlebars<'reg>,
87        _: &'rc crate::Context,
88        _: &mut crate::RenderContext<'reg, 'rc>,
89    ) -> std::result::Result<crate::ScopedJson<'rc>, crate::RenderError> {
90        let all_true = h.params().iter().all(|p| p.value().is_truthy(false));
91        Ok(crate::ScopedJson::Derived(crate::JsonValue::from(all_true)))
92    }
93}
94
95#[allow(non_camel_case_types)]
96pub struct or;
97
98impl crate::HelperDef for or {
99    fn call_inner<'reg: 'rc, 'rc>(
100        &self,
101        h: &crate::Helper<'rc>,
102        _r: &'reg crate::Handlebars<'reg>,
103        _: &'rc crate::Context,
104        _: &mut crate::RenderContext<'reg, 'rc>,
105    ) -> std::result::Result<crate::ScopedJson<'rc>, crate::RenderError> {
106        let any_true = h.params().iter().any(|p| p.value().is_truthy(false));
107        Ok(crate::ScopedJson::Derived(crate::JsonValue::from(any_true)))
108    }
109}
110
111#[cfg(test)]
112mod test_conditions {
113    fn test_condition(condition: &str, expected: bool) {
114        let handlebars = crate::Handlebars::new();
115
116        let result = handlebars
117            .render_template(
118                &format!("{{{{#if {condition}}}}}lorem{{{{else}}}}ipsum{{{{/if}}}}"),
119                &json!({}),
120            )
121            .unwrap();
122        assert_eq!(&result, if expected { "lorem" } else { "ipsum" });
123    }
124
125    #[test]
126    fn test_and_or() {
127        test_condition("(or (gt 3 5) (gt 5 3))", true);
128        test_condition("(and null 4)", false);
129        test_condition("(or null 4)", true);
130        test_condition("(and null 4 5 6)", false);
131        test_condition("(or null 4 5 6)", true);
132        test_condition("(and 1 2 3 4)", true);
133        test_condition("(or 1 2 3 4)", true);
134        test_condition("(and 1 2 3 4 0)", false);
135        test_condition("(or 1 2 3 4 0)", true);
136        test_condition("(or null 2 3 4 0)", true);
137        test_condition("(or [] [])", false);
138        test_condition("(or [1] [])", true);
139        test_condition("(or [1] [2])", true);
140        test_condition("(or [1] [2] [3])", true);
141        test_condition("(or [1] [2] [3] [4])", true);
142        test_condition("(or [1] [2] [3] [4] [])", true);
143    }
144
145    #[test]
146    fn test_cmp() {
147        test_condition("(gt 5 3)", true);
148        test_condition("(gt 3 5)", false);
149        test_condition("(not [])", true);
150    }
151
152    #[test]
153    fn test_eq() {
154        test_condition("(eq 5 5)", true);
155        test_condition("(eq 5 6)", false);
156        test_condition(r#"(eq "foo" "foo")"#, true);
157        test_condition(r#"(eq "foo" "Foo")"#, false);
158        test_condition(r"(eq [5] [5])", true);
159        test_condition(r"(eq [5] [4])", false);
160        test_condition(r#"(eq 5 "5")"#, false);
161        test_condition(r"(eq 5 [5])", false);
162    }
163
164    #[test]
165    fn test_ne() {
166        test_condition("(ne 5 6)", true);
167        test_condition("(ne 5 5)", false);
168        test_condition(r#"(ne "foo" "foo")"#, false);
169        test_condition(r#"(ne "foo" "Foo")"#, true);
170    }
171
172    #[test]
173    fn nested_conditions() {
174        let handlebars = crate::Handlebars::new();
175
176        let result = handlebars
177            .render_template("{{#if (gt 5 3)}}lorem{{else}}ipsum{{/if}}", &json!({}))
178            .unwrap();
179        assert_eq!(&result, "lorem");
180
181        let result = handlebars
182            .render_template(
183                "{{#if (not (gt 5 3))}}lorem{{else}}ipsum{{/if}}",
184                &json!({}),
185            )
186            .unwrap();
187        assert_eq!(&result, "ipsum");
188    }
189
190    #[test]
191    fn test_len() {
192        let handlebars = crate::Handlebars::new();
193
194        let result = handlebars
195            .render_template("{{len value}}", &json!({"value": [1,2,3]}))
196            .unwrap();
197        assert_eq!(&result, "3");
198
199        let result = handlebars
200            .render_template("{{len value}}", &json!({"value": {"a" :1, "b": 2}}))
201            .unwrap();
202        assert_eq!(&result, "2");
203
204        let result = handlebars
205            .render_template("{{len value}}", &json!({"value": "tomcat"}))
206            .unwrap();
207        assert_eq!(&result, "6");
208
209        let result = handlebars
210            .render_template("{{len value}}", &json!({"value": 3}))
211            .unwrap();
212        assert_eq!(&result, "0");
213    }
214
215    #[test]
216    fn test_comparisons() {
217        // Integer comparisons
218        test_condition("(gt 5 3)", true);
219        test_condition("(gt 3 5)", false);
220        test_condition("(gte 5 5)", true);
221        test_condition("(lt 3 5)", true);
222        test_condition("(lte 5 5)", true);
223        test_condition("(lt 9007199254740992 9007199254740993)", true);
224
225        // Float comparisons
226        test_condition("(gt 5.5 3.3)", true);
227        test_condition("(gt 3.3 5.5)", false);
228        test_condition("(gte 5.5 5.5)", true);
229        test_condition("(lt 3.3 5.5)", true);
230        test_condition("(lte 5.5 5.5)", true);
231
232        // String comparisons
233        test_condition(r#"(gt "b" "a")"#, true);
234        test_condition(r#"(lt "a" "b")"#, true);
235        test_condition(r#"(gte "a" "a")"#, true);
236
237        // Mixed type comparisons
238        test_condition(r#"(gt 53 "35")"#, true);
239        test_condition(r#"(lt 53 "35")"#, false);
240        test_condition(r#"(lt "35" 53)"#, true);
241        test_condition(r#"(gte "53" 53)"#, true);
242        test_condition(r#"(lt -1 0)"#, true);
243        test_condition(r#"(lt "-1" 0)"#, true);
244        test_condition(r#"(lt "-1.00" 0)"#, true);
245        test_condition(r#"(gt "1.00" 0)"#, true);
246        test_condition(r#"(gt 0 -1)"#, true);
247        test_condition(r#"(gt 0 "-1")"#, true);
248        test_condition(r#"(gt 0 "-1.00")"#, true);
249        test_condition(r#"(lt 0 "1.00")"#, true);
250        // u64::MAX
251        test_condition(r#"(gt 18446744073709551615 -1)"#, true);
252
253        // Boolean comparisons
254        test_condition("(gt true false)", true);
255        test_condition("(lt false true)", true);
256    }
257}