create_grafana_plugin/
template.rs1use anyhow::{Context, Result};
4use serde::Serialize;
5use std::path::{Path, PathBuf};
6use tera::Tera;
7use walkdir::WalkDir;
8
9use crate::config::ProjectConfig;
10
11pub fn templates_root() -> Result<PathBuf> {
17 let exe_dir = std::env::current_exe()
18 .ok()
19 .and_then(|p| p.parent().map(Path::to_path_buf));
20
21 if let Some(ref dir) = exe_dir {
22 let candidate = dir.join("templates");
23 if candidate.exists() {
24 return Ok(candidate);
25 }
26 if let Some(c) = dir
27 .parent()
28 .and_then(|p| p.parent())
29 .map(|p| p.join("templates"))
30 && c.exists()
31 {
32 return Ok(c);
33 }
34 }
35
36 let manifest_dir = env!("CARGO_MANIFEST_DIR");
37 let workspace_root = Path::new(manifest_dir)
38 .parent()
39 .unwrap_or_else(|| Path::new("."));
40 let candidate = workspace_root.join("templates");
41 if candidate.exists() {
42 return Ok(candidate);
43 }
44
45 anyhow::bail!("Could not find templates directory")
46}
47
48pub fn render_template_to_bytes(
54 src: &Path,
55 rel_path: &Path,
56 context: &TemplateContext,
57) -> Result<Vec<u8>> {
58 if is_binary_file(src) || rel_path.extension().and_then(|e| e.to_str()) != Some("tera") {
59 std::fs::read(src).with_context(|| format!("Failed to read file: {}", src.display()))
60 } else {
61 let template_body = std::fs::read_to_string(src)
62 .with_context(|| format!("Failed to read template: {}", src.display()))?;
63 let rendered = render_string(&template_body, context)?;
64 Ok(rendered.into_bytes())
65 }
66}
67
68#[derive(Debug, Serialize)]
70pub struct TemplateContext {
71 pub plugin_name: String,
73 pub plugin_description: String,
74 pub author: String,
75 pub org: String,
76 pub plugin_type: String,
77 pub has_wasm: bool,
78 pub has_docker: bool,
79 pub has_mock: bool,
80
81 pub plugin_id: String,
83 pub crate_name: String,
84 pub current_year: String,
85 pub today: String,
86 pub pascal_case_name: String,
87}
88
89impl TemplateContext {
90 pub fn from_config(config: &ProjectConfig) -> Self {
92 let plugin_id = format!("{}-{}", config.org, config.name);
93 let crate_name = config.name.replace('-', "_");
94 let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
95 let year = chrono::Utc::now().format("%Y").to_string();
96
97 let pascal_case_name = config
98 .name
99 .split('-')
100 .map(|word| {
101 let mut chars = word.chars();
102 match chars.next() {
103 None => String::new(),
104 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
105 }
106 })
107 .collect::<String>();
108
109 Self {
110 plugin_name: config.name.clone(),
111 plugin_description: config.description.clone(),
112 author: config.author.clone(),
113 org: config.org.clone(),
114 plugin_type: config.plugin_type.to_string(),
115 has_wasm: config.has_wasm,
116 has_docker: config.has_docker,
117 has_mock: config.has_mock,
118 plugin_id,
119 crate_name,
120 current_year: year,
121 today,
122 pascal_case_name,
123 }
124 }
125
126 pub fn apply_dates_from_existing_plugin_json(&mut self, project_dir: &Path) {
128 let plugin_path = project_dir.join("plugin.json");
129 let Ok(raw) = std::fs::read_to_string(&plugin_path) else {
130 return;
131 };
132 let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {
133 return;
134 };
135 let Some(u) = v
136 .get("info")
137 .and_then(|info| info.get("updated"))
138 .and_then(|x| x.as_str())
139 else {
140 return;
141 };
142 self.today = u.to_string();
143 if let Some(y) = u.split('-').next() {
144 self.current_year = y.to_string();
145 }
146 }
147}
148
149const BINARY_EXTENSIONS: &[&str] = &[
151 "svg", "png", "jpg", "jpeg", "gif", "ico", "woff", "woff2", "ttf", "eot",
152];
153
154fn is_binary_file(path: &Path) -> bool {
156 path.extension()
157 .and_then(|ext| ext.to_str())
158 .is_some_and(|ext| BINARY_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
159}
160
161pub fn collect_template_dirs(templates_root: &Path, dirs: &[&str]) -> Vec<(PathBuf, PathBuf)> {
163 let mut files = Vec::new();
164 for dir_name in dirs {
165 let dir = templates_root.join(dir_name);
166 if !dir.exists() {
167 continue;
168 }
169 for entry in WalkDir::new(&dir).into_iter().filter_map(Result::ok) {
170 if entry.file_type().is_file() {
171 let rel = entry.path().strip_prefix(&dir).unwrap_or(entry.path());
172 files.push((entry.path().to_path_buf(), rel.to_path_buf()));
173 }
174 }
175 }
176 files
177}
178
179pub fn render_string(template: &str, context: &TemplateContext) -> Result<String> {
185 let mut tera = Tera::default();
186 tera.add_raw_template("__inline__", template)
187 .context("Failed to parse template")?;
188 let tera_ctx =
189 tera::Context::from_serialize(context).context("Failed to serialize template context")?;
190 tera.render("__inline__", &tera_ctx)
191 .context("Failed to render template")
192}
193
194pub fn render_file(
200 src: &Path,
201 output_dir: &Path,
202 rel_path: &Path,
203 context: &TemplateContext,
204) -> Result<PathBuf> {
205 let out_rel = if rel_path.extension().and_then(|e| e.to_str()) == Some("tera") {
207 rel_path.with_extension("")
208 } else {
209 rel_path.to_path_buf()
210 };
211
212 let dest = output_dir.join(&out_rel);
213
214 if let Some(parent) = dest.parent() {
216 std::fs::create_dir_all(parent)
217 .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
218 }
219
220 if is_binary_file(src) || (rel_path.extension().and_then(|e| e.to_str()) != Some("tera")) {
221 std::fs::copy(src, &dest)
223 .with_context(|| format!("Failed to copy file: {}", src.display()))?;
224 } else {
225 let template_body = std::fs::read_to_string(src)
227 .with_context(|| format!("Failed to read template: {}", src.display()))?;
228 let rendered = render_string(&template_body, context)?;
229 std::fs::write(&dest, rendered)
230 .with_context(|| format!("Failed to write file: {}", dest.display()))?;
231 }
232
233 Ok(dest)
234}