1use 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 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 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 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 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 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 test_condition(r#"(gt 18446744073709551615 -1)"#, true);
414
415 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}