use crate::{TemplateContext, cli::ValidateFormat, functions, validator};
use minijinja::Environment;
use serde::Serialize;
use std::fs;
use std::io::{self, Read, Write};
pub fn render_template(
template_source: Option<&str>,
output_file: Option<&str>,
trust_mode: bool,
validate_format: Option<ValidateFormat>,
) -> Result<(), Box<dyn std::error::Error>> {
let template_content = read_template(template_source)?;
let template_context = match template_source {
Some(file_path) => TemplateContext::from_template_file(file_path, trust_mode)?,
None => TemplateContext::from_stdin(trust_mode)?,
};
let context = serde_json::json!({});
let rendered = render(
template_source,
&template_content,
&context,
template_context,
)?;
if let Some(format) = validate_format {
validator::validate_output(&rendered, format)?;
}
write_output(&rendered, output_file)?;
Ok(())
}
fn read_template(template_source: Option<&str>) -> Result<String, Box<dyn std::error::Error>> {
match template_source {
Some(file_path) => {
fs::read_to_string(file_path)
.map_err(|e| format!("Failed to read template file '{}': {}", file_path, e).into())
}
None => {
let mut buffer = String::new();
io::stdin()
.read_to_string(&mut buffer)
.map_err(|e| format!("Failed to read from stdin: {}", e))?;
if buffer.is_empty() {
return Err(
"No input provided. Either specify a template file or pipe content to stdin."
.into(),
);
}
Ok(buffer)
}
}
}
fn render(
template_source: Option<&str>,
template_content: &str,
context: &impl Serialize,
template_context: TemplateContext,
) -> Result<String, Box<dyn std::error::Error>> {
let mut env = Environment::new();
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
let loader_context = template_context.clone();
env.set_loader(move |name: &str| -> Result<Option<String>, minijinja::Error> {
if !loader_context.is_trust_mode() {
if name.starts_with('/') {
return Err(minijinja::Error::new(
minijinja::ErrorKind::InvalidOperation,
format!(
"Security: Absolute paths are not allowed: {}. Use --trust to bypass this restriction.",
name
),
));
}
if name.contains("..") {
return Err(minijinja::Error::new(
minijinja::ErrorKind::InvalidOperation,
format!(
"Security: Parent directory (..) traversal is not allowed: {}. Use --trust to bypass this restriction.",
name
),
));
}
}
let resolved_path = loader_context.resolve_path(name);
match fs::read_to_string(&resolved_path) {
Ok(content) => Ok(Some(content)),
Err(e) => {
Err(minijinja::Error::new(
minijinja::ErrorKind::TemplateNotFound,
format!("Failed to load template '{}': {}", resolved_path.display(), e),
))
}
}
});
functions::register_all(&mut env, template_context);
let template_name = template_source.unwrap_or("template");
env.add_template(template_name, template_content)
.map_err(|e| format_minijinja_error("Failed to parse template", &e))?;
let tmpl = env.get_template(template_name)?;
tmpl.render(context)
.map_err(|e| format_minijinja_error("Failed to render template", &e).into())
}
fn format_minijinja_error(prefix: &str, error: &minijinja::Error) -> String {
use std::fmt::Write;
let mut msg = String::new();
writeln!(&mut msg, "{}", prefix).ok();
writeln!(&mut msg).ok();
writeln!(&mut msg, "Error: {}", error).ok();
if let Some(detail) = error.detail() {
writeln!(&mut msg).ok();
writeln!(&mut msg, "{}", detail).ok();
}
msg
}
fn write_output(
rendered: &str,
output_file: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
match output_file {
Some(path) => {
fs::write(path, rendered)
.map_err(|e| format!("Failed to write output file '{}': {}", path, e))?;
eprintln!("Successfully rendered template to '{}'", path);
}
None => {
print!("{}", rendered);
io::stdout().flush()?;
}
}
Ok(())
}