Skip to content

Commit 0df9f4b

Browse files
authored
refactor(py/handlebarrz): move native helper definitions into separate helpers.rs mod (#289)
CHANGELOG: - [ ] Move helper definitions into helper mod.
1 parent 5eb2c87 commit 0df9f4b

File tree

2 files changed

+357
-341
lines changed

2 files changed

+357
-341
lines changed

python/handlebarrz/src/helpers.rs

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
17+
use handlebars::{
18+
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
19+
Renderable,
20+
};
21+
22+
/// Helper for comparing equality between two values.
23+
///
24+
/// Renders the template block if `arg1` is equal to `arg2`.
25+
/// Otherwise, it renders the inverse block (if provided).
26+
///
27+
/// ## Usage
28+
///
29+
/// ```handlebars
30+
/// {{#ifEquals arg1 arg2}}
31+
/// <p>arg1 is equal to arg2</p>
32+
/// {{else}}
33+
/// <p>arg1 is not equal to arg2</p>
34+
/// {{/ifEquals}}
35+
/// ```
36+
///
37+
/// ## Parameters
38+
///
39+
/// * `arg1`: The first argument to compare.
40+
/// * `arg2`: The second argument to compare.
41+
///
42+
/// The helper renders the template block if `arg1` is equal to `arg2`.
43+
/// Otherwise, it renders the inverse block (if provided).
44+
#[derive(Clone, Copy, Debug)]
45+
pub struct IfEqualsHelper {}
46+
47+
impl HelperDef for IfEqualsHelper {
48+
fn call<'reg: 'rc, 'rc>(
49+
&self,
50+
h: &Helper<'rc>,
51+
reg: &'reg Handlebars<'reg>,
52+
ctx: &'rc Context,
53+
rc: &mut RenderContext<'reg, 'rc>,
54+
out: &mut dyn Output,
55+
) -> Result<(), RenderError> {
56+
let first = h.param(0).ok_or_else(|| {
57+
RenderError::from(RenderErrorReason::ParamNotFoundForIndex("ifEquals", 0))
58+
})?;
59+
let second = h.param(1).ok_or_else(|| {
60+
RenderError::from(RenderErrorReason::ParamNotFoundForIndex("ifEquals", 1))
61+
})?;
62+
63+
if first.value() == second.value() {
64+
if let Some(template) = h.template() {
65+
template.render(reg, ctx, rc, out)?;
66+
}
67+
} else if let Some(template) = h.inverse() {
68+
template.render(reg, ctx, rc, out)?;
69+
}
70+
71+
Ok(())
72+
}
73+
}
74+
75+
#[cfg(test)]
76+
mod if_equals_tests {
77+
use super::*;
78+
use serde_json::json;
79+
80+
#[test]
81+
fn with_true_condition_renders_main_block() {
82+
let mut handlebars = Handlebars::new();
83+
handlebars.register_helper("ifEquals", Box::new(IfEqualsHelper {}));
84+
85+
assert_eq!(
86+
handlebars
87+
.render_template("{{#ifEquals 1 1}}yes{{else}}no{{/ifEquals}}", &json!({}))
88+
.unwrap(),
89+
"yes"
90+
);
91+
}
92+
93+
#[test]
94+
fn with_false_condition_renders_else_block() {
95+
let mut handlebars = Handlebars::new();
96+
handlebars.register_helper("ifEquals", Box::new(IfEqualsHelper {}));
97+
98+
assert_eq!(
99+
handlebars
100+
.render_template("{{#ifEquals 1 2}}yes{{else}}no{{/ifEquals}}", &json!({}))
101+
.unwrap(),
102+
"no"
103+
);
104+
}
105+
106+
#[test]
107+
fn with_false_condition_and_no_else_renders_empty_string() {
108+
let mut handlebars = Handlebars::new();
109+
handlebars.register_helper("ifEquals", Box::new(IfEqualsHelper {}));
110+
111+
assert_eq!(
112+
handlebars
113+
.render_template("{{#ifEquals 1 2}}yes{{/ifEquals}}", &json!({}))
114+
.unwrap(),
115+
""
116+
);
117+
}
118+
}
119+
/// Helper for comparing inequality between two values.
120+
///
121+
/// Renders the template block if `arg1` is not equal to `arg2`.
122+
/// Otherwise, it renders the inverse block (if provided).
123+
///
124+
/// ## Usage
125+
///
126+
/// ```handlebars
127+
/// {{#unlessEquals arg1 arg2}}
128+
/// <p>arg1 is not equal to arg2</p>
129+
/// {{else}}
130+
/// <p>arg1 is equal to arg2</p>
131+
/// {{/unlessEquals}}
132+
/// ```
133+
///
134+
/// ## Parameters
135+
///
136+
/// * `arg1`: The first argument to compare.
137+
/// * `arg2`: The second argument to compare.
138+
///
139+
/// The helper renders the template block if `arg1` is not equal to `arg2`.
140+
/// Otherwise, it renders the inverse block (if provided).
141+
#[derive(Clone, Copy, Debug)]
142+
pub struct UnlessEqualsHelper {}
143+
144+
impl HelperDef for UnlessEqualsHelper {
145+
fn call<'reg: 'rc, 'rc>(
146+
&self,
147+
h: &Helper<'rc>,
148+
reg: &'reg Handlebars<'reg>,
149+
ctx: &'rc Context,
150+
rc: &mut RenderContext<'reg, 'rc>,
151+
out: &mut dyn Output,
152+
) -> Result<(), RenderError> {
153+
let first = h.param(0).ok_or_else(|| {
154+
RenderError::from(RenderErrorReason::ParamNotFoundForIndex("unlessEquals", 0))
155+
})?;
156+
let second = h.param(1).ok_or_else(|| {
157+
RenderError::from(RenderErrorReason::ParamNotFoundForIndex("unlessEquals", 1))
158+
})?;
159+
160+
if first.value() != second.value() {
161+
if let Some(template) = h.template() {
162+
template.render(reg, ctx, rc, out)?;
163+
}
164+
} else if let Some(template) = h.inverse() {
165+
template.render(reg, ctx, rc, out)?;
166+
}
167+
168+
Ok(())
169+
}
170+
}
171+
172+
#[cfg(test)]
173+
mod unless_equals_tests {
174+
use super::*;
175+
use serde_json::json;
176+
177+
#[test]
178+
fn with_false_condition_renders_main_block() {
179+
let mut handlebars = Handlebars::new();
180+
handlebars.register_helper("unlessEquals", Box::new(UnlessEqualsHelper {}));
181+
182+
assert_eq!(
183+
handlebars
184+
.render_template(
185+
"{{#unlessEquals 1 2}}yes{{else}}no{{/unlessEquals}}",
186+
&json!({})
187+
)
188+
.unwrap(),
189+
"yes"
190+
);
191+
}
192+
193+
#[test]
194+
fn with_true_condition_renders_else_block() {
195+
let mut handlebars = Handlebars::new();
196+
handlebars.register_helper("unlessEquals", Box::new(UnlessEqualsHelper {}));
197+
198+
assert_eq!(
199+
handlebars
200+
.render_template(
201+
"{{#unlessEquals 1 1}}yes{{else}}no{{/unlessEquals}}",
202+
&json!({})
203+
)
204+
.unwrap(),
205+
"no"
206+
);
207+
}
208+
209+
#[test]
210+
fn with_true_condition_and_no_else_renders_empty_string() {
211+
let mut handlebars = Handlebars::new();
212+
handlebars.register_helper("unlessEquals", Box::new(UnlessEqualsHelper {}));
213+
214+
assert_eq!(
215+
handlebars
216+
.render_template("{{#unlessEquals 1 1}}yes{{/unlessEquals}}", &json!({}))
217+
.unwrap(),
218+
""
219+
);
220+
}
221+
}
222+
/// Helper to serialize data to a JSON string.
223+
///
224+
/// ## Usage
225+
///
226+
/// ```handlebars
227+
/// <script type="application/json">
228+
/// {{json data indent=2}}
229+
/// </script>
230+
/// ```
231+
///
232+
/// ## Parameters
233+
///
234+
/// * `data`: The data to serialize to JSON.
235+
///
236+
/// ## Hash Arguments
237+
///
238+
/// * `indent`: Optional. If provided, the JSON output will be pretty-printed
239+
/// with the specified indent level (integer). If not provided, the JSON
240+
/// output will be compact (no whitespace).
241+
///
242+
/// This helper is useful for embedding JSON data directly into templates,
243+
/// for example, to pass configuration or data to client-side JavaScript code.
244+
#[derive(Clone, Copy, Debug)]
245+
pub struct JsonHelper {}
246+
247+
impl HelperDef for JsonHelper {
248+
fn call<'reg: 'rc, 'rc>(
249+
&self,
250+
h: &Helper<'rc>,
251+
_reg: &'reg Handlebars<'reg>,
252+
_ctx: &'rc Context,
253+
_rc: &mut RenderContext<'reg, 'rc>,
254+
out: &mut dyn Output,
255+
) -> Result<(), RenderError> {
256+
let param = match h.param(0) {
257+
Some(p) => p.value(),
258+
None => {
259+
out.write("")?;
260+
return Ok(());
261+
}
262+
};
263+
264+
let indent_param = h.hash_get("indent");
265+
let use_pretty = indent_param.is_some();
266+
let result = if use_pretty {
267+
serde_json::to_string_pretty(param)
268+
} else {
269+
serde_json::to_string(param)
270+
};
271+
let json_str = result.unwrap_or_else(|_| "{}".to_string());
272+
out.write(&json_str)?;
273+
Ok(())
274+
}
275+
}
276+
277+
#[cfg(test)]
278+
mod json_tests {
279+
use super::*;
280+
use serde_json::json;
281+
282+
#[test]
283+
fn renders_object_as_json() {
284+
let mut handlebars = Handlebars::new();
285+
handlebars.register_helper("json", Box::new(JsonHelper {}));
286+
287+
let data = json!({"a": 1, "b": 2});
288+
let rendered = handlebars.render_template("{{json this}}", &data).unwrap();
289+
assert_eq!(rendered, r#"{"a":1,"b":2}"#);
290+
}
291+
292+
#[test]
293+
fn renders_object_with_indent() {
294+
let mut handlebars = Handlebars::new();
295+
handlebars.register_helper("json", Box::new(JsonHelper {}));
296+
297+
let data = json!({"a": 1, "b": 2});
298+
let rendered_indent = handlebars
299+
.render_template("{{json this indent=2}}", &data)
300+
.unwrap();
301+
assert!(rendered_indent.contains("\"a\": 1"));
302+
assert!(rendered_indent.contains("\"b\": 2"));
303+
}
304+
305+
#[test]
306+
fn handles_empty_params() {
307+
let mut handlebars = Handlebars::new();
308+
handlebars.register_helper("json", Box::new(JsonHelper {}));
309+
310+
let rendered_empty_params = handlebars.render_template("{{json}}", &json!({})).unwrap();
311+
assert_eq!(rendered_empty_params, "");
312+
}
313+
314+
#[test]
315+
fn renders_array_as_json() {
316+
let mut handlebars = Handlebars::new();
317+
handlebars.register_helper("json", Box::new(JsonHelper {}));
318+
319+
let array_data = json!([1, 2, 3]);
320+
let rendered_array = handlebars
321+
.render_template("{{json this}}", &array_data)
322+
.unwrap();
323+
assert_eq!(rendered_array, r#"[1,2,3]"#);
324+
}
325+
326+
#[test]
327+
fn renders_array_with_indent() {
328+
let mut handlebars = Handlebars::new();
329+
handlebars.register_helper("json", Box::new(JsonHelper {}));
330+
331+
let array_data = json!([1, 2, 3]);
332+
let rendered_array_pretty = handlebars
333+
.render_template("{{json this indent=2}}", &array_data)
334+
.unwrap();
335+
assert!(rendered_array_pretty.contains("1,"));
336+
assert!(rendered_array_pretty.contains("2,"));
337+
assert!(rendered_array_pretty.contains("3"));
338+
}
339+
340+
#[test]
341+
fn renders_empty_object() {
342+
let mut handlebars = Handlebars::new();
343+
handlebars.register_helper("json", Box::new(JsonHelper {}));
344+
345+
let empty_map = json!({});
346+
let rendered_empty = handlebars
347+
.render_template("{{json this}}", &empty_map)
348+
.unwrap();
349+
assert_eq!(rendered_empty, "{}");
350+
}
351+
}

0 commit comments

Comments
 (0)