use anyhow::{Context, Result};
use serde::Serialize;
use std::path::{Path, PathBuf};
use tera::Tera;
use walkdir::WalkDir;
use crate::config::ProjectConfig;
pub fn templates_root() -> Result<PathBuf> {
let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(Path::to_path_buf));
if let Some(ref dir) = exe_dir {
let candidate = dir.join("templates");
if candidate.exists() {
return Ok(candidate);
}
if let Some(c) = dir
.parent()
.and_then(|p| p.parent())
.map(|p| p.join("templates"))
&& c.exists()
{
return Ok(c);
}
}
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = Path::new(manifest_dir)
.parent()
.unwrap_or_else(|| Path::new("."));
let candidate = workspace_root.join("templates");
if candidate.exists() {
return Ok(candidate);
}
anyhow::bail!("Could not find templates directory")
}
pub fn render_template_to_bytes(
src: &Path,
rel_path: &Path,
context: &TemplateContext,
) -> Result<Vec<u8>> {
if is_binary_file(src) || rel_path.extension().and_then(|e| e.to_str()) != Some("tera") {
std::fs::read(src).with_context(|| format!("Failed to read file: {}", src.display()))
} else {
let template_body = std::fs::read_to_string(src)
.with_context(|| format!("Failed to read template: {}", src.display()))?;
let rendered = render_string(&template_body, context)?;
Ok(rendered.into_bytes())
}
}
#[derive(Debug, Serialize)]
pub struct TemplateContext {
pub plugin_name: String,
pub plugin_description: String,
pub author: String,
pub org: String,
pub plugin_type: String,
pub has_wasm: bool,
pub has_docker: bool,
pub has_mock: bool,
pub plugin_id: String,
pub crate_name: String,
pub current_year: String,
pub today: String,
pub pascal_case_name: String,
}
impl TemplateContext {
pub fn from_config(config: &ProjectConfig) -> Self {
let plugin_id = format!("{}-{}", config.org, config.name);
let crate_name = config.name.replace('-', "_");
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
let year = chrono::Utc::now().format("%Y").to_string();
let pascal_case_name = config
.name
.split('-')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<String>();
Self {
plugin_name: config.name.clone(),
plugin_description: config.description.clone(),
author: config.author.clone(),
org: config.org.clone(),
plugin_type: config.plugin_type.to_string(),
has_wasm: config.has_wasm,
has_docker: config.has_docker,
has_mock: config.has_mock,
plugin_id,
crate_name,
current_year: year,
today,
pascal_case_name,
}
}
pub fn apply_dates_from_existing_plugin_json(&mut self, project_dir: &Path) {
let plugin_path = project_dir.join("plugin.json");
let Ok(raw) = std::fs::read_to_string(&plugin_path) else {
return;
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {
return;
};
let Some(u) = v
.get("info")
.and_then(|info| info.get("updated"))
.and_then(|x| x.as_str())
else {
return;
};
self.today = u.to_string();
if let Some(y) = u.split('-').next() {
self.current_year = y.to_string();
}
}
}
const BINARY_EXTENSIONS: &[&str] = &[
"svg", "png", "jpg", "jpeg", "gif", "ico", "woff", "woff2", "ttf", "eot",
];
fn is_binary_file(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| BINARY_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
}
pub fn collect_template_dirs(templates_root: &Path, dirs: &[&str]) -> Vec<(PathBuf, PathBuf)> {
let mut files = Vec::new();
for dir_name in dirs {
let dir = templates_root.join(dir_name);
if !dir.exists() {
continue;
}
for entry in WalkDir::new(&dir).into_iter().filter_map(Result::ok) {
if entry.file_type().is_file() {
let rel = entry.path().strip_prefix(&dir).unwrap_or(entry.path());
files.push((entry.path().to_path_buf(), rel.to_path_buf()));
}
}
}
files
}
pub fn render_string(template: &str, context: &TemplateContext) -> Result<String> {
let mut tera = Tera::default();
tera.add_raw_template("__inline__", template)
.context("Failed to parse template")?;
let tera_ctx =
tera::Context::from_serialize(context).context("Failed to serialize template context")?;
tera.render("__inline__", &tera_ctx)
.context("Failed to render template")
}
pub fn render_file(
src: &Path,
output_dir: &Path,
rel_path: &Path,
context: &TemplateContext,
) -> Result<PathBuf> {
let out_rel = if rel_path.extension().and_then(|e| e.to_str()) == Some("tera") {
rel_path.with_extension("")
} else {
rel_path.to_path_buf()
};
let dest = output_dir.join(&out_rel);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {}", parent.display()))?;
}
if is_binary_file(src) || (rel_path.extension().and_then(|e| e.to_str()) != Some("tera")) {
std::fs::copy(src, &dest)
.with_context(|| format!("Failed to copy file: {}", src.display()))?;
} else {
let template_body = std::fs::read_to_string(src)
.with_context(|| format!("Failed to read template: {}", src.display()))?;
let rendered = render_string(&template_body, context)?;
std::fs::write(&dest, rendered)
.with_context(|| format!("Failed to write file: {}", dest.display()))?;
}
Ok(dest)
}