use crate::context::TemplateContext;
use crate::error::{Result, TemplateError};
use crate::renderer::{render_template, TemplateRenderer};
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
pub fn render(template: &str, vars: HashMap<&str, &str>) -> Result<String> {
let mut json_vars = HashMap::new();
for (key, value) in vars {
json_vars.insert(key.to_string(), Value::String(value.to_string()));
}
render_template(template, json_vars)
}
pub fn render_with_json(template: &str, vars: HashMap<&str, Value>) -> Result<String> {
let mut json_vars = HashMap::new();
for (key, value) in vars {
json_vars.insert(key.to_string(), value);
}
render_template(template, json_vars)
}
pub fn render_file<P: AsRef<Path>>(path: P, vars: HashMap<&str, &str>) -> Result<String> {
let mut json_vars = HashMap::new();
for (key, value) in vars {
json_vars.insert(key.to_string(), Value::String(value.to_string()));
}
crate::renderer::render_template_file(path.as_ref(), json_vars)
}
pub fn render_with_context(template: &str, _context: &TemplateContext) -> Result<String> {
let mut renderer = TemplateRenderer::new()?;
renderer.render_str(template, "template")
}
pub fn render_to_format(
template: &str,
vars: HashMap<&str, &str>,
format: OutputFormat,
) -> Result<String> {
let mut json_vars = HashMap::new();
for (key, value) in vars {
json_vars.insert(key.to_string(), Value::String(value.to_string()));
}
let rendered = render_template(template, json_vars)?;
match format {
OutputFormat::Toml => Ok(rendered),
OutputFormat::Json => convert_to_json(&rendered),
OutputFormat::Yaml => convert_to_yaml(&rendered),
OutputFormat::Plain => strip_template_syntax(&rendered),
}
}
#[derive(Debug, Clone, Copy)]
pub enum OutputFormat {
Toml,
Json,
Yaml,
Plain,
}
pub fn convert_to_json(toml_content: &str) -> Result<String> {
let parsed: Value = toml::from_str(toml_content).map_err(|e| {
TemplateError::ValidationError(format!("Failed to parse TOML for JSON conversion: {}", e))
})?;
serde_json::to_string_pretty(&parsed)
.map_err(|e| TemplateError::ValidationError(format!("Failed to serialize to JSON: {}", e)))
}
pub fn convert_to_yaml(toml_content: &str) -> Result<String> {
let parsed: Value = toml::from_str(toml_content).map_err(|e| {
TemplateError::ValidationError(format!("Failed to parse TOML for YAML conversion: {}", e))
})?;
serde_yaml::to_string(&parsed)
.map_err(|e| TemplateError::ValidationError(format!("Failed to serialize to YAML: {}", e)))
}
pub fn strip_template_syntax(content: &str) -> Result<String> {
let mut result = String::new();
let mut in_braces = false;
let mut brace_depth = 0;
for ch in content.chars() {
match ch {
'{' => {
if let Some(next) = content.chars().nth(result.len() + 1) {
if next == '{' || next == '%' {
in_braces = true;
brace_depth = 1;
continue;
}
}
}
'}' => {
if in_braces {
if let Some(prev) = content.chars().nth(result.len() - 1) {
if prev == '}' || prev == '%' {
brace_depth -= 1;
if brace_depth == 0 {
in_braces = false;
}
continue;
}
}
}
}
_ => {
if !in_braces {
result.push(ch);
}
}
}
}
Ok(result)
}
pub struct TemplateBuilder {
template: Option<String>,
variables: HashMap<String, Value>,
format: OutputFormat,
context: Option<TemplateContext>,
}
impl Default for TemplateBuilder {
fn default() -> Self {
Self {
template: None,
variables: HashMap::new(),
format: OutputFormat::Toml,
context: None,
}
}
}
impl TemplateBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn template<S: Into<String>>(mut self, template: S) -> Self {
self.template = Some(template.into());
self
}
pub fn variable<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.variables
.insert(key.into(), Value::String(value.into()));
self
}
pub fn json_variable<K: Into<String>>(mut self, key: K, value: Value) -> Self {
self.variables.insert(key.into(), value);
self
}
pub fn variables<I, K, V>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
for (key, value) in vars {
self.variables
.insert(key.into(), Value::String(value.into()));
}
self
}
pub fn format(mut self, format: OutputFormat) -> Self {
self.format = format;
self
}
pub fn context(mut self, context: TemplateContext) -> Self {
self.context = Some(context);
self
}
pub fn render(self) -> Result<String> {
let template = self
.template
.ok_or_else(|| TemplateError::ValidationError("No template provided".to_string()))?;
if let Some(context) = self.context {
render_with_context(&template, &context)
} else {
let rendered = render_template(&template, self.variables)?;
match self.format {
OutputFormat::Toml => Ok(rendered),
OutputFormat::Json => convert_to_json(&rendered),
OutputFormat::Yaml => convert_to_yaml(&rendered),
OutputFormat::Plain => strip_template_syntax(&rendered),
}
}
}
pub fn render_file<P: AsRef<Path>>(self, path: P) -> Result<String> {
let _template = self
.template
.ok_or_else(|| TemplateError::ValidationError("No template provided".to_string()))?;
let mut json_vars = HashMap::new();
for (key, value) in self.variables {
json_vars.insert(key, value);
}
let result = crate::renderer::render_template_file(path.as_ref(), json_vars)?;
match self.format {
OutputFormat::Toml => Ok(result),
OutputFormat::Json => convert_to_json(&result),
OutputFormat::Yaml => convert_to_yaml(&result),
OutputFormat::Plain => strip_template_syntax(&result),
}
}
}
pub mod quick {
use super::*;
pub fn greeting(name: &str) -> String {
render(
"Hello {{ name }}!",
[("name", name)].iter().cloned().collect(),
)
.unwrap_or_default()
}
pub fn config(service: &str, port: u16) -> String {
render(
"[service]\nname = \"{{ service }}\"\nport = {{ port }}",
[("service", service), ("port", &port.to_string())]
.iter()
.cloned()
.collect(),
)
.unwrap_or_default()
}
pub fn json_template(name: &str, value: &str) -> String {
render_to_format(
"{\"name\": \"{{ name }}\", \"value\": \"{{ value }}\"}",
[("name", name), ("value", value)].iter().cloned().collect(),
OutputFormat::Json,
)
.unwrap_or_default()
}
pub fn yaml_template(title: &str, items: Vec<&str>) -> String {
let _items_str = items.join("\", \"");
render_to_format(
"title: {{ title }}\nitems:\n - \"{{ items | join('\",\n - \"') }}\"",
[("title", title)].iter().cloned().collect(),
OutputFormat::Yaml,
)
.unwrap_or_default()
}
}
#[macro_export]
macro_rules! template {
($template:expr) => {
$template
};
($template:expr, $($key:ident = $value:expr),* $(,)?) => {{
let mut vars = std::collections::HashMap::new();
$(
vars.insert(stringify!($key).to_string(), serde_json::Value::String($value.to_string()));
)*
$crate::render_template($template, vars).unwrap_or_else(|_| $template.to_string())
}};
}
#[macro_export]
macro_rules! template_literal {
($template:expr) => {
$template
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_render() {
let result = render(
"Hello {{ name }}!",
[("name", "World")].iter().cloned().collect(),
)
.unwrap();
assert_eq!(result, "Hello World!");
}
#[test]
fn test_render_with_json() {
let mut vars = HashMap::new();
vars.insert(
"items",
Value::Array(vec![
Value::String("apple".to_string()),
Value::String("banana".to_string()),
]),
);
let result = render_with_json("Items: {{ items | length }}", vars).unwrap();
assert!(result.contains("Items:"));
}
#[test]
fn test_template_builder() {
let result = TemplateBuilder::new()
.template("Service: {{ service }}, Port: {{ port }}")
.variable("service", "my-service")
.variable("port", "8080")
.render()
.unwrap();
assert_eq!(result, "Service: my-service, Port: 8080");
}
#[test]
fn test_output_formats() {
let toml_result = render_to_format(
"name = \"{{ name }}\"",
[("name", "test")].iter().cloned().collect(),
OutputFormat::Toml,
)
.unwrap();
assert_eq!(toml_result, "name = \"test\"");
let json_result = render_to_format(
"{\"name\": \"{{ name }}\"}",
[("name", "test")].iter().cloned().collect(),
OutputFormat::Json,
)
.unwrap();
assert!(json_result.contains("\"name\""));
assert!(json_result.contains("\"test\""));
}
#[test]
fn test_quick_templates() {
let greeting = quick::greeting("Alice");
assert_eq!(greeting, "Hello Alice!");
let config = quick::config("web-server", 3000);
assert!(config.contains("web-server"));
assert!(config.contains("3000"));
}
}