quickstart_lib/tools/
helpers.rs

1//! Handlebars template helpers
2//!
3//! This module provides custom helpers for use in handlebars templates,
4//! such as text case conversion utilities.
5
6use handlebars::{Context, Helper, HelperDef, Output, RenderContext, RenderError};
7use thiserror::Error;
8
9#[allow(dead_code)]
10/// Error type for helpers
11#[derive(Debug, Error)]
12pub enum HelperError {
13    /// Error when lowercase character conversion fails
14    #[error("Failed to convert character to lowercase")]
15    LowercaseConversionError,
16}
17
18/// Helper to convert text to lowercase
19#[derive(Clone, Copy)]
20pub struct LowercaseHelper;
21
22impl HelperDef for LowercaseHelper {
23    fn call<'reg: 'rc, 'rc>(
24        &self,
25        h: &Helper<'rc>,
26        _: &'reg handlebars::Handlebars<'reg>,
27        _: &'rc Context,
28        _: &mut RenderContext<'reg, 'rc>,
29        out: &mut dyn Output,
30    ) -> handlebars::HelperResult {
31        let param = h.param(0).ok_or_else(|| {
32            RenderError::from(handlebars::RenderErrorReason::ParamNotFoundForIndex(
33                "lowercase",
34                0,
35            ))
36        })?;
37
38        let value = param.value().as_str().unwrap_or_default().to_lowercase();
39        out.write(&value)?;
40
41        Ok(())
42    }
43}
44
45/// Helper to convert text to uppercase
46#[derive(Clone, Copy)]
47pub struct UppercaseHelper;
48
49impl HelperDef for UppercaseHelper {
50    fn call<'reg: 'rc, 'rc>(
51        &self,
52        h: &Helper<'rc>,
53        _: &'reg handlebars::Handlebars<'reg>,
54        _: &'rc Context,
55        _: &mut RenderContext<'reg, 'rc>,
56        out: &mut dyn Output,
57    ) -> handlebars::HelperResult {
58        let param = h.param(0).ok_or_else(|| {
59            RenderError::from(handlebars::RenderErrorReason::ParamNotFoundForIndex(
60                "uppercase",
61                0,
62            ))
63        })?;
64
65        let value = param.value().as_str().unwrap_or_default().to_uppercase();
66        out.write(&value)?;
67
68        Ok(())
69    }
70}
71
72/// Helper to convert text to snake_case
73#[derive(Clone, Copy)]
74pub struct SnakeCaseHelper;
75
76impl HelperDef for SnakeCaseHelper {
77    fn call<'reg: 'rc, 'rc>(
78        &self,
79        h: &Helper<'rc>,
80        _: &'reg handlebars::Handlebars<'reg>,
81        _: &'rc Context,
82        _: &mut RenderContext<'reg, 'rc>,
83        out: &mut dyn Output,
84    ) -> handlebars::HelperResult {
85        let param = h.param(0).ok_or_else(|| {
86            RenderError::from(handlebars::RenderErrorReason::ParamNotFoundForIndex(
87                "snake_case",
88                0,
89            ))
90        })?;
91
92        let input = param.value().as_str().unwrap_or_default();
93        let value = to_snake_case(input);
94        out.write(&value)?;
95
96        Ok(())
97    }
98}
99
100/// Helper to convert text to kebab-case
101#[derive(Clone, Copy)]
102pub struct KebabCaseHelper;
103
104impl HelperDef for KebabCaseHelper {
105    fn call<'reg: 'rc, 'rc>(
106        &self,
107        h: &Helper<'rc>,
108        _: &'reg handlebars::Handlebars<'reg>,
109        _: &'rc Context,
110        _: &mut RenderContext<'reg, 'rc>,
111        out: &mut dyn Output,
112    ) -> handlebars::HelperResult {
113        let param = h.param(0).ok_or_else(|| {
114            RenderError::from(handlebars::RenderErrorReason::ParamNotFoundForIndex(
115                "kebab_case",
116                0,
117            ))
118        })?;
119
120        let input = param.value().as_str().unwrap_or_default();
121        let value = to_kebab_case(input);
122        out.write(&value)?;
123
124        Ok(())
125    }
126}
127
128/// Convert a string to snake_case
129pub fn to_snake_case(input: &str) -> String {
130    let mut result = String::new();
131    let mut prev_is_sep = false;
132    for (i, c) in input.char_indices() {
133        if c.is_uppercase() {
134            if i > 0 && !prev_is_sep {
135                result.push('_');
136            }
137            if let Some(lc) = c.to_lowercase().next() {
138                result.push(lc);
139            }
140            prev_is_sep = false;
141        } else if c == '-' || c == ' ' || c == '_' {
142            if !prev_is_sep {
143                result.push('_');
144                prev_is_sep = true;
145            }
146        } else {
147            result.push(c);
148            prev_is_sep = false;
149        }
150    }
151    result
152}
153
154/// Convert a string to kebab-case
155pub fn to_kebab_case(input: &str) -> String {
156    let mut result = String::new();
157    let mut prev_is_sep = false;
158    for (i, c) in input.char_indices() {
159        if c.is_uppercase() {
160            if i > 0 && !prev_is_sep {
161                result.push('-');
162            }
163            if let Some(lc) = c.to_lowercase().next() {
164                result.push(lc);
165            }
166            prev_is_sep = false;
167        } else if c == '_' || c == ' ' || c == '-' {
168            if !prev_is_sep {
169                result.push('-');
170                prev_is_sep = true;
171            }
172        } else {
173            result.push(c);
174            prev_is_sep = false;
175        }
176    }
177    result
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use handlebars::Handlebars;
184    use pretty_assertions::assert_eq;
185
186    #[test]
187    fn test_lowercase_helper() {
188        let mut handlebars = Handlebars::new();
189        handlebars.register_helper("lowercase", Box::new(LowercaseHelper));
190
191        let template = "{{lowercase value}}";
192        let mut data = std::collections::HashMap::new();
193        data.insert("value", "TEST-PROJECT");
194
195        let result = handlebars.render_template(template, &data).unwrap();
196        assert_eq!(result, "test-project");
197    }
198
199    #[test]
200    fn test_uppercase_helper() {
201        let mut handlebars = Handlebars::new();
202        handlebars.register_helper("uppercase", Box::new(UppercaseHelper));
203
204        let template = "{{uppercase value}}";
205        let mut data = std::collections::HashMap::new();
206        data.insert("value", "test-project");
207
208        let result = handlebars.render_template(template, &data).unwrap();
209        assert_eq!(result, "TEST-PROJECT");
210    }
211
212    #[test]
213    fn test_snake_case_helper() {
214        let mut handlebars = Handlebars::new();
215        handlebars.register_helper("snake_case", Box::new(SnakeCaseHelper));
216
217        let template = "{{snake_case value}}";
218        let mut data = std::collections::HashMap::new();
219        data.insert("value", "TestProject");
220
221        let result = handlebars.render_template(template, &data).unwrap();
222        assert_eq!(result, "test_project");
223    }
224
225    #[test]
226    fn test_kebab_case_helper() {
227        let mut handlebars = Handlebars::new();
228        handlebars.register_helper("kebab_case", Box::new(KebabCaseHelper));
229
230        let template = "{{kebab_case value}}";
231        let mut data = std::collections::HashMap::new();
232        data.insert("value", "test_project");
233
234        let result = handlebars.render_template(template, &data).unwrap();
235        assert_eq!(result, "test-project");
236    }
237
238    #[test]
239    fn test_helper_missing_param() {
240        let mut handlebars = Handlebars::new();
241        handlebars.register_helper("lowercase", Box::new(LowercaseHelper));
242        handlebars.register_helper("uppercase", Box::new(UppercaseHelper));
243        handlebars.register_helper("snake_case", Box::new(SnakeCaseHelper));
244        handlebars.register_helper("kebab_case", Box::new(KebabCaseHelper));
245
246        let helpers = ["lowercase", "uppercase", "snake_case", "kebab_case"];
247        for helper in helpers {
248            let template = format!("{{{{{helper}}}}}"); // No param
249            let result = handlebars
250                .render_template(&template, &std::collections::HashMap::<&str, &str>::new());
251            assert!(
252                result.is_err(),
253                "helper '{helper}' should error on missing param"
254            );
255        }
256    }
257
258    #[test]
259    fn test_helper_non_string_param() {
260        let mut handlebars = Handlebars::new();
261        handlebars.register_helper("lowercase", Box::new(LowercaseHelper));
262        handlebars.register_helper("uppercase", Box::new(UppercaseHelper));
263        handlebars.register_helper("snake_case", Box::new(SnakeCaseHelper));
264        handlebars.register_helper("kebab_case", Box::new(KebabCaseHelper));
265
266        let mut data = std::collections::HashMap::new();
267        data.insert("value", 123);
268
269        let helpers = ["lowercase", "uppercase", "snake_case", "kebab_case"];
270        for helper in helpers {
271            let template = format!("{{{{{helper} value}}}}");
272            let result = handlebars.render_template(&template, &data);
273            // Should not panic, should handle gracefully (output empty string)
274            assert!(result.is_ok());
275            assert_eq!(result.unwrap(), "");
276        }
277    }
278
279    #[test]
280    fn test_to_snake_case_edge_cases() {
281        assert_eq!(to_snake_case("").as_str(), "");
282        assert_eq!(to_snake_case("Already_Snake").as_str(), "already_snake");
283        assert_eq!(to_snake_case("with-dash").as_str(), "with_dash");
284        assert_eq!(to_snake_case("with space").as_str(), "with_space");
285        assert_eq!(to_snake_case("123ABC").as_str(), "123_a_b_c");
286    }
287
288    #[test]
289    fn test_to_kebab_case_edge_cases() {
290        assert_eq!(to_kebab_case("").as_str(), "");
291        assert_eq!(to_kebab_case("Already-Kebab").as_str(), "already-kebab");
292        assert_eq!(to_kebab_case("with_underscore").as_str(), "with-underscore");
293        assert_eq!(to_kebab_case("with space").as_str(), "with-space");
294        assert_eq!(to_kebab_case("123ABC").as_str(), "123-a-b-c");
295    }
296}