create_grafana_plugin/
template.rs1use anyhow::{Context, Result};
6use include_dir::{Dir, DirEntry, include_dir};
7use serde::Serialize;
8use std::path::{Path, PathBuf};
9use tera::Tera;
10
11use crate::config::ProjectConfig;
12
13static TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
15
16pub fn render_to_bytes(
25 contents: &[u8],
26 rel_path: &Path,
27 context: &TemplateContext,
28) -> Result<Vec<u8>> {
29 if is_binary_path(rel_path) || rel_path.extension().and_then(|e| e.to_str()) != Some("tera") {
30 Ok(contents.to_vec())
31 } else {
32 let template_body =
33 std::str::from_utf8(contents).context("Template file is not valid UTF-8")?;
34 let rendered = render_string(template_body, context)?;
35 Ok(rendered.into_bytes())
36 }
37}
38
39#[derive(Debug, Serialize)]
41pub struct TemplateContext {
42 pub plugin_name: String,
44 pub plugin_description: String,
45 pub author: String,
46 pub org: String,
47 pub plugin_type: String,
48 pub has_wasm: bool,
49 pub has_docker: bool,
50 pub has_mock: bool,
51 pub port_offset: u16,
52
53 pub plugin_id: String,
55 pub crate_name: String,
56 pub current_year: String,
57 pub today: String,
58 pub pascal_case_name: String,
59}
60
61impl TemplateContext {
62 pub fn from_config(config: &ProjectConfig) -> Self {
64 let plugin_id = format!("{}-{}", config.org, config.name);
65 let crate_name = config.name.replace('-', "_");
66 let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
67 let year = chrono::Utc::now().format("%Y").to_string();
68
69 let pascal_case_name = config
70 .name
71 .split('-')
72 .map(|word| {
73 let mut chars = word.chars();
74 match chars.next() {
75 None => String::new(),
76 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
77 }
78 })
79 .collect::<String>();
80
81 Self {
82 plugin_name: config.name.clone(),
83 plugin_description: config.description.clone(),
84 author: config.author.clone(),
85 org: config.org.clone(),
86 plugin_type: config.plugin_type.to_string(),
87 has_wasm: config.has_wasm,
88 has_docker: config.has_docker,
89 has_mock: config.has_mock,
90 port_offset: config.port_offset,
91 plugin_id,
92 crate_name,
93 current_year: year,
94 today,
95 pascal_case_name,
96 }
97 }
98
99 pub fn apply_dates_from_existing_plugin_json(&mut self, project_dir: &Path) {
101 let plugin_path = project_dir.join("plugin.json");
102 let Ok(raw) = std::fs::read_to_string(&plugin_path) else {
103 return;
104 };
105 let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {
106 return;
107 };
108 let Some(u) = v
109 .get("info")
110 .and_then(|info| info.get("updated"))
111 .and_then(|x| x.as_str())
112 else {
113 return;
114 };
115 self.today = u.to_string();
116 if let Some(y) = u.split('-').next() {
117 self.current_year = y.to_string();
118 }
119 }
120}
121
122const BINARY_EXTENSIONS: &[&str] = &[
124 "svg", "png", "jpg", "jpeg", "gif", "ico", "woff", "woff2", "ttf", "eot",
125];
126
127fn is_binary_path(path: &Path) -> bool {
129 path.extension()
130 .and_then(|ext| ext.to_str())
131 .is_some_and(|ext| BINARY_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
132}
133
134fn walk_embedded_dir(dir: &'static Dir<'static>) -> Vec<&'static include_dir::File<'static>> {
136 let mut out = Vec::new();
137 for entry in dir.entries() {
138 match entry {
139 DirEntry::Dir(d) => out.extend(walk_embedded_dir(d)),
140 DirEntry::File(f) => out.push(f),
141 }
142 }
143 out
144}
145
146pub fn collect_template_files(dirs: &[&str]) -> Vec<(&'static [u8], PathBuf)> {
151 let mut files = Vec::new();
152 for dir_name in dirs {
153 let Some(dir) = TEMPLATES.get_dir(dir_name) else {
154 continue;
155 };
156 for file in walk_embedded_dir(dir) {
157 let rel = file.path().strip_prefix(dir_name).unwrap_or(file.path());
158 files.push((file.contents(), rel.to_path_buf()));
159 }
160 }
161 files
162}
163
164pub fn render_string(template: &str, context: &TemplateContext) -> Result<String> {
170 let mut tera = Tera::default();
171 tera.add_raw_template("__inline__", template)
172 .context("Failed to parse template")?;
173 let tera_ctx =
174 tera::Context::from_serialize(context).context("Failed to serialize template context")?;
175 tera.render("__inline__", &tera_ctx)
176 .context("Failed to render template")
177}
178
179pub fn write_rendered(
187 contents: &[u8],
188 rel_path: &Path,
189 output_dir: &Path,
190 context: &TemplateContext,
191) -> Result<PathBuf> {
192 let out_rel = if rel_path.extension().and_then(|e| e.to_str()) == Some("tera") {
193 rel_path.with_extension("")
194 } else {
195 rel_path.to_path_buf()
196 };
197
198 let dest = output_dir.join(&out_rel);
199
200 if let Some(parent) = dest.parent() {
201 std::fs::create_dir_all(parent)
202 .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
203 }
204
205 let bytes = render_to_bytes(contents, rel_path, context)?;
206 std::fs::write(&dest, bytes)
207 .with_context(|| format!("Failed to write file: {}", dest.display()))?;
208
209 Ok(dest)
210}