use anyhow::Result;
use handlebars::Handlebars;
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
pub struct TemplateEngine {
handlebars: Handlebars<'static>,
templates_dir: PathBuf,
}
impl TemplateEngine {
pub fn new(templates_dir: PathBuf) -> Result<Self> {
let mut handlebars = Handlebars::new();
handlebars.register_helper("camelCase", Box::new(camel_case_helper));
handlebars.register_helper("pascalCase", Box::new(pascal_case_helper));
handlebars.register_helper("snakeCase", Box::new(snake_case_helper));
handlebars.register_helper("kebabCase", Box::new(kebab_case_helper));
Ok(Self {
handlebars,
templates_dir,
})
}
pub fn render_template(&mut self, template_name: &str, context: &Value) -> Result<String> {
let template_path = self.templates_dir.join(template_name);
let template_content = fs::read_to_string(&template_path)?;
self.handlebars
.register_template_string(template_name, template_content)?;
let rendered = self.handlebars.render(template_name, context)?;
Ok(rendered)
}
pub fn render_to_file(
&mut self,
template_name: &str,
context: &Value,
output_path: &Path,
) -> Result<()> {
let rendered = self.render_template(template_name, context)?;
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(output_path, rendered)?;
Ok(())
}
}
fn camel_case_helper(
h: &handlebars::Helper,
_: &Handlebars,
_: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
let value = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
handlebars::RenderErrorReason::Other(
"camelCase helper requires a string parameter".to_string(),
)
})?;
let camel_case = to_camel_case(value);
out.write(&camel_case)?;
Ok(())
}
fn pascal_case_helper(
h: &handlebars::Helper,
_: &Handlebars,
_: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
let value = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
handlebars::RenderErrorReason::Other(
"pascalCase helper requires a string parameter".to_string(),
)
})?;
let pascal_case = to_pascal_case(value);
out.write(&pascal_case)?;
Ok(())
}
fn snake_case_helper(
h: &handlebars::Helper,
_: &Handlebars,
_: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
let value = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
handlebars::RenderErrorReason::Other(
"snakeCase helper requires a string parameter".to_string(),
)
})?;
let snake_case = to_snake_case(value);
out.write(&snake_case)?;
Ok(())
}
fn kebab_case_helper(
h: &handlebars::Helper,
_: &Handlebars,
_: &handlebars::Context,
_: &mut handlebars::RenderContext,
out: &mut dyn handlebars::Output,
) -> handlebars::HelperResult {
let value = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
handlebars::RenderErrorReason::Other(
"kebabCase helper requires a string parameter".to_string(),
)
})?;
let kebab_case = to_kebab_case(value);
out.write(&kebab_case)?;
Ok(())
}
fn to_camel_case(s: &str) -> String {
let pascal = to_pascal_case(s);
if pascal.is_empty() {
return pascal;
}
let mut chars = pascal.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
}
}
fn to_pascal_case(s: &str) -> String {
s.split(|c: char| !c.is_alphanumeric())
.filter(|s| !s.is_empty())
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => {
first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
}
}
})
.collect()
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
let mut prev_was_upper = false;
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 && !prev_was_upper {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
prev_was_upper = true;
} else if c.is_alphanumeric() {
result.push(c);
prev_was_upper = false;
} else if !result.is_empty() && !result.ends_with('_') {
result.push('_');
prev_was_upper = false;
}
}
result.trim_end_matches('_').to_string()
}
fn to_kebab_case(s: &str) -> String {
to_snake_case(s).replace('_', "-")
}