use crate::{HandlerError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TemplateEngineType {
Tera,
Handlebars,
Liquid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateConfig {
pub engine: TemplateEngineType,
pub template_dir: String,
pub cache_enabled: bool,
pub autoescape: bool,
pub custom_filters: Vec<String>,
}
pub struct TemplateEngine {
config: TemplateConfig,
#[cfg(feature = "tera")]
tera: Option<tera::Tera>,
#[cfg(feature = "handlebars")]
handlebars: Option<handlebars::Handlebars<'static>>,
}
impl TemplateEngine {
pub fn new(template_dir: &str) -> Result<Self> {
let config = TemplateConfig {
engine: TemplateEngineType::Tera, template_dir: template_dir.to_string(),
cache_enabled: true,
autoescape: true,
custom_filters: vec![],
};
Self::with_config(config)
}
pub fn with_config(config: TemplateConfig) -> Result<Self> {
let mut engine = Self {
config: config.clone(),
#[cfg(feature = "tera")]
tera: None,
#[cfg(feature = "handlebars")]
handlebars: None,
};
engine.initialize()?;
Ok(engine)
}
fn initialize(&mut self) -> Result<()> {
match self.config.engine {
#[cfg(feature = "tera")]
TemplateEngineType::Tera => {
let mut tera = tera::Tera::new(&format!("{}/**/*.html", self.config.template_dir))
.map_err(|e| HandlerError::Jsonnet(format!("Tera initialization error: {}", e)))?;
if self.config.autoescape {
tera.autoescape_on(vec![".html", ".htm"]);
}
for filter in &self.config.custom_filters {
}
self.tera = Some(tera);
}
#[cfg(feature = "handlebars")]
TemplateEngineType::Handlebars => {
let mut handlebars = handlebars::Handlebars::new();
handlebars
.register_templates_directory(".html", &self.config.template_dir)
.map_err(|e| HandlerError::Jsonnet(format!("Handlebars initialization error: {}", e)))?;
for helper in &self.config.custom_filters {
}
self.handlebars = Some(handlebars);
}
#[cfg(not(any(feature = "tera", feature = "handlebars")))]
_ => {
return Err(HandlerError::Jsonnet(
"No template engine features enabled. Enable 'tera' or 'handlebars' feature.".to_string()
));
}
}
Ok(())
}
pub fn render(&self, template_name: &str, context: &serde_json::Value) -> Result<String> {
match self.config.engine {
#[cfg(feature = "tera")]
TemplateEngineType::Tera => {
if let Some(tera) = &self.tera {
let context = tera::Context::from_serialize(context)
.map_err(|e| HandlerError::Jsonnet(format!("Context serialization error: {}", e)))?;
tera.render(template_name, &context)
.map_err(|e| HandlerError::Jsonnet(format!("Template rendering error: {}", e)))
} else {
Err(HandlerError::Jsonnet("Tera engine not initialized".to_string()))
}
}
#[cfg(feature = "handlebars")]
TemplateEngineType::Handlebars => {
if let Some(handlebars) = &self.handlebars {
handlebars.render(template_name, context)
.map_err(|e| HandlerError::Jsonnet(format!("Template rendering error: {}", e)))
} else {
Err(HandlerError::Jsonnet("Handlebars engine not initialized".to_string()))
}
}
#[cfg(not(any(feature = "tera", feature = "handlebars")))]
_ => {
Err(HandlerError::Jsonnet(
"No template engine features enabled".to_string()
))
}
}
}
pub fn render_with_hashmap(&self, template_name: &str, context: &HashMap<String, serde_json::Value>) -> Result<String> {
let json_context: serde_json::Value = serde_json::to_value(context)
.map_err(|e| HandlerError::Jsonnet(format!("Context conversion error: {}", e)))?;
self.render(template_name, &json_context)
}
pub fn render_with_layout(&self, template_name: &str, layout_name: &str, context: &serde_json::Value) -> Result<String> {
let content = self.render(template_name, context)?;
let mut layout_context = context.clone();
if let Some(obj) = layout_context.as_object_mut() {
obj.insert("content".to_string(), serde_json::Value::String(content));
}
self.render(layout_name, &layout_context)
}
pub fn render_partial(&self, template_name: &str, context: &serde_json::Value) -> Result<String> {
self.render(template_name, context)
}
pub fn has_template(&self, template_name: &str) -> bool {
match self.config.engine {
#[cfg(feature = "tera")]
TemplateEngineType::Tera => {
self.tera.as_ref().map_or(false, |tera| tera.get_template_names().any(|name| name == template_name))
}
#[cfg(feature = "handlebars")]
TemplateEngineType::Handlebars => {
self.handlebars.as_ref().map_or(false, |hb| hb.has_template(template_name))
}
#[cfg(not(any(feature = "tera", feature = "handlebars")))]
_ => false,
}
}
pub fn get_template_names(&self) -> Vec<String> {
match self.config.engine {
#[cfg(feature = "tera")]
TemplateEngineType::Tera => {
self.tera.as_ref()
.map(|tera| tera.get_template_names().map(|s| s.to_string()).collect())
.unwrap_or_default()
}
#[cfg(feature = "handlebars")]
TemplateEngineType::Handlebars => {
vec![]
}
#[cfg(not(any(feature = "tera", feature = "handlebars")))]
_ => vec![],
}
}
#[cfg(feature = "tera")]
pub fn register_tera_filter<F>(&mut self, name: &str, filter: F) -> Result<()>
where
F: tera::Filter + Send + Sync + 'static,
{
if let Some(tera) = &mut self.tera {
tera.register_filter(name, filter);
Ok(())
} else {
Err(HandlerError::Jsonnet("Tera engine not initialized".to_string()))
}
}
#[cfg(feature = "tera")]
pub fn register_tera_function<F>(&mut self, name: &str, function: F) -> Result<()>
where
F: tera::Function + Send + Sync + 'static,
{
if let Some(tera) = &mut self.tera {
tera.register_function(name, function);
Ok(())
} else {
Err(HandlerError::Jsonnet("Tera engine not initialized".to_string()))
}
}
#[cfg(feature = "handlebars")]
pub fn register_handlebars_helper<F>(&mut self, name: &str, helper: F) -> Result<()>
where
F: handlebars::HelperDef + Send + Sync + 'static,
{
if let Some(handlebars) = &mut self.handlebars {
handlebars.register_helper(name, Box::new(helper));
Ok(())
} else {
Err(HandlerError::Jsonnet("Handlebars engine not initialized".to_string()))
}
}
pub fn reload_templates(&mut self) -> Result<()> {
self.initialize()
}
pub fn config(&self) -> &TemplateConfig {
&self.config
}
pub fn engine_type(&self) -> &TemplateEngineType {
&self.config.engine
}
}
pub fn render_simple_template(template: &str, context: &HashMap<String, String>) -> Result<String> {
let mut result = template.to_string();
for (key, value) in context {
let placeholder = format!("{{{{{}}}}}", key);
result = result.replace(&placeholder, value);
}
Ok(result)
}
pub fn json_to_hashmap(json: &serde_json::Value) -> Result<HashMap<String, String>> {
let mut result = HashMap::new();
if let Some(obj) = json.as_object() {
for (key, value) in obj {
if let Some(str_value) = value.as_str() {
result.insert(key.clone(), str_value.to_string());
} else {
result.insert(key.clone(), value.to_string());
}
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_simple_template_rendering() {
let template = "<h1>{{title}}</h1><p>{{content}}</p>";
let mut context = HashMap::new();
context.insert("title".to_string(), "Hello".to_string());
context.insert("content".to_string(), "World".to_string());
let result = render_simple_template(template, &context).unwrap();
assert_eq!(result, "<h1>Hello</h1><p>World</p>");
}
#[test]
fn test_json_to_hashmap() {
let json = json!({
"name": "Alice",
"age": 30,
"city": "Tokyo"
});
let result = json_to_hashmap(&json).unwrap();
assert_eq!(result.get("name").unwrap(), "Alice");
assert_eq!(result.get("age").unwrap(), "30");
assert_eq!(result.get("city").unwrap(), "Tokyo");
}
#[cfg(feature = "tera")]
#[test]
fn test_template_config_creation() {
let config = TemplateConfig {
engine: TemplateEngineType::Tera,
template_dir: "templates".to_string(),
cache_enabled: true,
autoescape: true,
custom_filters: vec!["markdown".to_string()],
};
assert_eq!(config.template_dir, "templates");
assert!(config.cache_enabled);
assert!(config.autoescape);
}
#[cfg(feature = "handlebars")]
#[test]
fn test_handlebars_config_creation() {
let config = TemplateConfig {
engine: TemplateEngineType::Handlebars,
template_dir: "templates".to_string(),
cache_enabled: false,
autoescape: false,
custom_filters: vec![],
};
assert_eq!(config.template_dir, "templates");
assert!(!config.cache_enabled);
assert!(!config.autoescape);
}
}