handlebars/helpers/
helper_extras.rs

1//! Helpers for boolean operations
2
3use std::cmp::Ordering;
4use std::iter::Iterator;
5use std::str::FromStr;
6
7use num_order::NumOrd;
8use serde_json::Value as Json;
9
10use crate::json::value::JsonTruthy;
11use crate::Renderable;
12
13#[derive(Clone, Copy)]
14pub struct BinaryBoolHelper {
15    name: &'static str,
16    op: fn(&Json, &Json) -> bool,
17}
18
19impl crate::HelperDef for BinaryBoolHelper {
20    fn call<'reg: 'rc, 'rc>(
21        &self,
22        h: &crate::Helper<'rc>,
23        r: &'reg crate::registry::Registry<'reg>,
24        ctx: &'rc crate::Context,
25        rc: &mut crate::RenderContext<'reg, 'rc>,
26        out: &mut dyn crate::Output,
27    ) -> crate::HelperResult {
28        let value = self.call_inner(h, r, ctx, rc)?;
29        let value = value.as_json().as_bool().unwrap_or(false);
30
31        if !(h.is_block()) {
32            return out
33                .write(value.to_string().as_str())
34                .map_err(|e| crate::RenderErrorReason::Other(e.to_string()).into());
35        }
36
37        let tmpl = if value { h.template() } else { h.inverse() };
38        match tmpl {
39            Some(t) => t.render(r, ctx, rc, out),
40            None => Ok(()),
41        }
42    }
43
44    fn call_inner<'reg: 'rc, 'rc>(
45        &self,
46        h: &crate::Helper<'rc>,
47        r: &'reg crate::registry::Registry<'reg>,
48        _ctx: &'rc crate::Context,
49        _rc: &mut crate::RenderContext<'reg, 'rc>,
50    ) -> Result<crate::ScopedJson<'rc>, crate::RenderError> {
51        let x = h
52            .param(0)
53            .and_then(|it| {
54                if r.strict_mode() && it.is_value_missing() {
55                    None
56                } else {
57                    Some(it.value())
58                }
59            })
60            .ok_or_else(|| crate::RenderErrorReason::ParamNotFoundForIndex(self.name, 0))?;
61        let y = h
62            .param(1)
63            .and_then(|it| {
64                if r.strict_mode() && it.is_value_missing() {
65                    None
66                } else {
67                    Some(it.value())
68                }
69            })
70            .ok_or_else(|| crate::RenderErrorReason::ParamNotFoundForIndex(self.name, 1))?;
71
72        Ok(crate::ScopedJson::Derived(Json::Bool((self.op)(x, y))))
73    }
74}
75
76pub(crate) static EQ_HELPER: BinaryBoolHelper = BinaryBoolHelper {
77    name: "eq",
78    op: |x, y| x == y,
79};
80pub(crate) static NEQ_HELPER: BinaryBoolHelper = BinaryBoolHelper {
81    name: "ne",
82    op: |x, y| x != y,
83};
84pub(crate) static GT_HELPER: BinaryBoolHelper = BinaryBoolHelper {
85    name: "gt",
86    op: |x, y| compare_json(x, y) == Some(Ordering::Greater),
87};
88pub(crate) static GTE_HELPER: BinaryBoolHelper = BinaryBoolHelper {
89    name: "gte",
90    op: |x, y| compare_json(x, y).is_some_and(|ord| ord != Ordering::Less),
91};
92pub(crate) static LT_HELPER: BinaryBoolHelper = BinaryBoolHelper {
93    name: "lt",
94    op: |x, y| compare_json(x, y) == Some(Ordering::Less),
95};
96pub(crate) static LTE_HELPER: BinaryBoolHelper = BinaryBoolHelper {
97    name: "lte",
98    op: |x, y| compare_json(x, y).is_some_and(|ord| ord != Ordering::Greater),
99};
100
101#[derive(Clone, Copy)]
102pub struct UnaryBoolHelper {
103    name: &'static str,
104    op: fn(&Json) -> bool,
105}
106
107impl crate::HelperDef for UnaryBoolHelper {
108    fn call<'reg: 'rc, 'rc>(
109        &self,
110        h: &crate::Helper<'rc>,
111        r: &'reg crate::registry::Registry<'reg>,
112        ctx: &'rc crate::Context,
113        rc: &mut crate::RenderContext<'reg, 'rc>,
114        out: &mut dyn crate::Output,
115    ) -> crate::HelperResult {
116        let value = self.call_inner(h, r, ctx, rc)?;
117        let value = value.as_json().as_bool().unwrap_or(false);
118
119        if !(h.is_block()) {
120            return out
121                .write(value.to_string().as_str())
122                .map_err(|e| crate::RenderErrorReason::Other(e.to_string()).into());
123        }
124
125        let tmpl = if value { h.template() } else { h.inverse() };
126        match tmpl {
127            Some(t) => t.render(r, ctx, rc, out),
128            None => Ok(()),
129        }
130    }
131
132    fn call_inner<'reg: 'rc, 'rc>(
133        &self,
134        h: &crate::Helper<'rc>,
135        r: &'reg crate::Handlebars<'reg>,
136        _: &'rc crate::Context,
137        _: &mut crate::RenderContext<'reg, 'rc>,
138    ) -> std::result::Result<crate::ScopedJson<'rc>, crate::RenderError> {
139        let arg = h
140            .param(0)
141            .and_then(|it| {
142                if r.strict_mode() && it.is_value_missing() {
143                    None
144                } else {
145                    Some(it.value())
146                }
147            })
148            .ok_or_else(|| crate::RenderErrorReason::ParamNotFoundForIndex(self.name, 0))?;
149        let result = (self.op)(arg);
150        Ok(crate::ScopedJson::Derived(crate::JsonValue::from(result)))
151    }
152}
153
154pub(crate) static NOT_HELPER: UnaryBoolHelper = UnaryBoolHelper {
155    name: "not",
156    op: |x| !x.is_truthy(false),
157};
158
159handlebars_helper!(len: |x: Json| {
160    match x {
161        Json::Array(a) => a.len(),
162        Json::Object(m) => m.len(),
163        Json::String(s) => s.len(),
164        _ => 0
165    }
166});
167
168fn compare_json(x: &Json, y: &Json) -> Option<Ordering> {
169    fn cmp_num_str(a_num: &serde_json::Number, b_str: &str) -> Option<Ordering> {
170        let b_num = serde_json::Number::from_str(b_str).ok()?;
171        cmp_nums(a_num, &b_num)
172    }
173
174    // this function relies on serde_json::Numbers coerce logic
175    // for number value between [0, u64::MAX], is_u64() returns true
176    // for number value between [i64::MIN, i64::MAX], is_i64() returns true
177    // for others, is_f64() returns true, note that this behaviour is not
178    //  guaranteed according to serde_json docs
179    fn cmp_nums(a_num: &serde_json::Number, b_num: &serde_json::Number) -> Option<Ordering> {
180        if a_num.is_u64() {
181            let a = a_num.as_u64()?;
182            if b_num.is_u64() {
183                NumOrd::num_partial_cmp(&a, &b_num.as_u64()?)
184            } else if b_num.is_i64() {
185                NumOrd::num_partial_cmp(&a, &b_num.as_i64()?)
186            } else {
187                NumOrd::num_partial_cmp(&a, &b_num.as_f64()?)
188            }
189        } else if a_num.is_i64() {
190            let a = a_num.as_i64()?;
191            if b_num.is_u64() {
192                NumOrd::num_partial_cmp(&a, &b_num.as_u64()?)
193            } else if b_num.is_i64() {
194                NumOrd::num_partial_cmp(&a, &b_num.as_i64()?)
195            } else {
196                NumOrd::num_partial_cmp(&a, &b_num.as_f64()?)
197            }
198        } else {
199            let a = a_num.as_f64()?;
200            if b_num.is_u64() {
201                NumOrd::num_partial_cmp(&a, &b_num.as_u64()?)
202            } else if b_num.is_i64() {
203                NumOrd::num_partial_cmp(&a, &b_num.as_i64()?)
204            } else {
205                NumOrd::num_partial_cmp(&a, &b_num.as_f64()?)
206            }
207        }
208    }
209
210    match (x, y) {
211        (Json::Number(a), Json::Number(b)) => cmp_nums(a, b),
212        (Json::String(a), Json::String(b)) => Some(a.cmp(b)),
213        (Json::Bool(a), Json::Bool(b)) => Some(a.cmp(b)),
214        (Json::Number(a), Json::String(b)) => cmp_num_str(a, b),
215        (Json::String(a), Json::Number(b)) => cmp_num_str(b, a).map(Ordering::reverse),
216        _ => None,
217    }
218}
219
220#[derive(Clone, Copy)]
221pub struct ManyBoolHelper {
222    name: &'static str,
223    op: fn(&Vec<crate::PathAndJson<'_>>) -> bool,
224}
225
226impl crate::HelperDef for ManyBoolHelper {
227    fn call<'reg: 'rc, 'rc>(
228        &self,
229        h: &crate::Helper<'rc>,
230        r: &'reg crate::registry::Registry<'reg>,
231        ctx: &'rc crate::Context,
232        rc: &mut crate::RenderContext<'reg, 'rc>,
233        out: &mut dyn crate::Output,
234    ) -> crate::HelperResult {
235        let value = self.call_inner(h, r, ctx, rc)?;
236        let value = value.as_json().as_bool().unwrap_or(false);
237
238        if !(h.is_block()) {
239            return out
240                .write(value.to_string().as_str())
241                .map_err(|e| crate::RenderErrorReason::Other(e.to_string()).into());
242        }
243
244        let tmpl = if value { h.template() } else { h.inverse() };
245        match tmpl {
246            Some(t) => t.render(r, ctx, rc, out),
247            None => Ok(()),
248        }
249    }
250
251    fn call_inner<'reg: 'rc, 'rc>(
252        &self,
253        h: &crate::Helper<'rc>,
254        _r: &'reg crate::Handlebars<'reg>,
255        _: &'rc crate::Context,
256        _: &mut crate::RenderContext<'reg, 'rc>,
257    ) -> std::result::Result<crate::ScopedJson<'rc>, crate::RenderError> {
258        let result = (self.op)(h.params());
259        Ok(crate::ScopedJson::Derived(crate::JsonValue::from(result)))
260    }
261}
262
263pub(crate) static AND_HELPER: ManyBoolHelper = ManyBoolHelper {
264    name: "and",
265    op: |params| params.iter().all(|p| p.value().is_truthy(false)),
266};
267
268pub(crate) static OR_HELPER: ManyBoolHelper = ManyBoolHelper {
269    name: "or",
270    op: |params| params.iter().any(|p| p.value().is_truthy(false)),
271};
272
273#[cfg(test)]
274mod test_conditions {
275    fn test_condition(condition: &str, expected: bool) {
276        let handlebars = crate::Handlebars::new();
277
278        let result = handlebars
279            .render_template(
280                &format!("{{{{#if {condition}}}}}lorem{{{{else}}}}ipsum{{{{/if}}}}"),
281                &json!({}),
282            )
283            .unwrap();
284        assert_eq!(&result, if expected { "lorem" } else { "ipsum" });
285    }
286
287    #[test]
288    fn test_and_or() {
289        test_condition("(or (gt 3 5) (gt 5 3))", true);
290        test_condition("(and null 4)", false);
291        test_condition("(or null 4)", true);
292        test_condition("(and null 4 5 6)", false);
293        test_condition("(or null 4 5 6)", true);
294        test_condition("(and 1 2 3 4)", true);
295        test_condition("(or 1 2 3 4)", true);
296        test_condition("(and 1 2 3 4 0)", false);
297        test_condition("(or 1 2 3 4 0)", true);
298        test_condition("(or null 2 3 4 0)", true);
299        test_condition("(or [] [])", false);
300        test_condition("(or [1] [])", true);
301        test_condition("(or [1] [2])", true);
302        test_condition("(or [1] [2] [3])", true);
303        test_condition("(or [1] [2] [3] [4])", true);
304        test_condition("(or [1] [2] [3] [4] [])", true);
305    }
306
307    #[test]
308    fn test_cmp() {
309        test_condition("(gt 5 3)", true);
310        test_condition("(gt 3 5)", false);
311        test_condition("(not [])", true);
312    }
313
314    #[test]
315    fn test_eq() {
316        test_condition("(eq 5 5)", true);
317        test_condition("(eq 5 6)", false);
318        test_condition(r#"(eq "foo" "foo")"#, true);
319        test_condition(r#"(eq "foo" "Foo")"#, false);
320        test_condition(r"(eq [5] [5])", true);
321        test_condition(r"(eq [5] [4])", false);
322        test_condition(r#"(eq 5 "5")"#, false);
323        test_condition(r"(eq 5 [5])", false);
324    }
325
326    #[test]
327    fn test_ne() {
328        test_condition("(ne 5 6)", true);
329        test_condition("(ne 5 5)", false);
330        test_condition(r#"(ne "foo" "foo")"#, false);
331        test_condition(r#"(ne "foo" "Foo")"#, true);
332    }
333
334    #[test]
335    fn nested_conditions() {
336        let handlebars = crate::Handlebars::new();
337
338        let result = handlebars
339            .render_template("{{#if (gt 5 3)}}lorem{{else}}ipsum{{/if}}", &json!({}))
340            .unwrap();
341        assert_eq!(&result, "lorem");
342
343        let result = handlebars
344            .render_template(
345                "{{#if (not (gt 5 3))}}lorem{{else}}ipsum{{/if}}",
346                &json!({}),
347            )
348            .unwrap();
349        assert_eq!(&result, "ipsum");
350    }
351
352    #[test]
353    fn test_len() {
354        let handlebars = crate::Handlebars::new();
355
356        let result = handlebars
357            .render_template("{{len value}}", &json!({"value": [1,2,3]}))
358            .unwrap();
359        assert_eq!(&result, "3");
360
361        let result = handlebars
362            .render_template("{{len value}}", &json!({"value": {"a" :1, "b": 2}}))
363            .unwrap();
364        assert_eq!(&result, "2");
365
366        let result = handlebars
367            .render_template("{{len value}}", &json!({"value": "tomcat"}))
368            .unwrap();
369        assert_eq!(&result, "6");
370
371        let result = handlebars
372            .render_template("{{len value}}", &json!({"value": 3}))
373            .unwrap();
374        assert_eq!(&result, "0");
375    }
376
377    #[test]
378    fn test_comparisons() {
379        // Integer comparisons
380        test_condition("(gt 5 3)", true);
381        test_condition("(gt 3 5)", false);
382        test_condition("(gte 5 5)", true);
383        test_condition("(lt 3 5)", true);
384        test_condition("(lte 5 5)", true);
385        test_condition("(lt 9007199254740992 9007199254740993)", true);
386
387        // Float comparisons
388        test_condition("(gt 5.5 3.3)", true);
389        test_condition("(gt 3.3 5.5)", false);
390        test_condition("(gte 5.5 5.5)", true);
391        test_condition("(lt 3.3 5.5)", true);
392        test_condition("(lte 5.5 5.5)", true);
393
394        // String comparisons
395        test_condition(r#"(gt "b" "a")"#, true);
396        test_condition(r#"(lt "a" "b")"#, true);
397        test_condition(r#"(gte "a" "a")"#, true);
398
399        // Mixed type comparisons
400        test_condition(r#"(gt 53 "35")"#, true);
401        test_condition(r#"(lt 53 "35")"#, false);
402        test_condition(r#"(lt "35" 53)"#, true);
403        test_condition(r#"(gte "53" 53)"#, true);
404        test_condition(r#"(lt -1 0)"#, true);
405        test_condition(r#"(lt "-1" 0)"#, true);
406        test_condition(r#"(lt "-1.00" 0)"#, true);
407        test_condition(r#"(gt "1.00" 0)"#, true);
408        test_condition(r#"(gt 0 -1)"#, true);
409        test_condition(r#"(gt 0 "-1")"#, true);
410        test_condition(r#"(gt 0 "-1.00")"#, true);
411        test_condition(r#"(lt 0 "1.00")"#, true);
412        // u64::MAX
413        test_condition(r#"(gt 18446744073709551615 -1)"#, true);
414
415        // Boolean comparisons
416        test_condition("(gt true false)", true);
417        test_condition("(lt false true)", true);
418    }
419
420    fn test_block(template: &str, expected: &str) {
421        let handlebars = crate::Handlebars::new();
422
423        let result = handlebars.render_template(template, &json!({})).unwrap();
424        assert_eq!(&result, expected);
425    }
426
427    #[test]
428    fn test_chained_else_support() {
429        test_block("{{#eq 1 1}}OK{{else}}KO{{/eq}}", "OK");
430        test_block("{{#eq 1 3}}OK{{else}}KO{{/eq}}", "KO");
431
432        test_block("{{#ne 1 1}}OK{{else}}KO{{/ne}}", "KO");
433        test_block("{{#ne 1 3}}OK{{else}}KO{{/ne}}", "OK");
434
435        test_block("{{#gt 2 1}}OK{{else}}KO{{/gt}}", "OK");
436        test_block("{{#gt 1 1}}OK{{else}}KO{{/gt}}", "KO");
437
438        test_block("{{#gte 2 1}}OK{{else}}KO{{/gte}}", "OK");
439        test_block("{{#gte 1 1}}OK{{else}}KO{{/gte}}", "OK");
440        test_block("{{#gte 0 1}}OK{{else}}KO{{/gte}}", "KO");
441
442        test_block("{{#lt 1 2}}OK{{else}}KO{{/lt}}", "OK");
443        test_block("{{#lt 2 2}}OK{{else}}KO{{/lt}}", "KO");
444
445        test_block("{{#lte 0 1}}OK{{else}}KO{{/lte}}", "OK");
446        test_block("{{#lte 1 1}}OK{{else}}KO{{/lte}}", "OK");
447        test_block("{{#lte 2 1}}OK{{else}}KO{{/lte}}", "KO");
448
449        test_block("{{#and true}}OK{{else}}KO{{/and}}", "OK");
450        test_block("{{#and true true}}OK{{else}}KO{{/and}}", "OK");
451        test_block("{{#and true true true}}OK{{else}}KO{{/and}}", "OK");
452        test_block("{{#and true true false}}OK{{else}}KO{{/and}}", "KO");
453        test_block("{{#and true false true}}OK{{else}}KO{{/and}}", "KO");
454        test_block("{{#and false false}}OK{{else}}KO{{/and}}", "KO");
455        test_block("{{#and false}}OK{{else}}KO{{/and}}", "KO");
456
457        test_block("{{#or true}}OK{{else}}KO{{/or}}", "OK");
458        test_block("{{#or true true}}OK{{else}}KO{{/or}}", "OK");
459        test_block("{{#or true true true}}OK{{else}}KO{{/or}}", "OK");
460        test_block("{{#or true true false}}OK{{else}}KO{{/or}}", "OK");
461        test_block("{{#or true false true}}OK{{else}}KO{{/or}}", "OK");
462        test_block("{{#or false true}}OK{{else}}KO{{/or}}", "OK");
463        test_block("{{#or false false}}OK{{else}}KO{{/or}}", "KO");
464        test_block("{{#or false}}OK{{else}}KO{{/or}}", "KO");
465
466        test_block("{{#not false}}OK{{else}}KO{{/not}}", "OK");
467        test_block("{{#not true}}OK{{else}}KO{{/not}}", "KO");
468    }
469}