1use crate::common::types::FieldValue;
2use anyhow::Result;
3use log::{debug, error, info};
4use std::{collections::HashMap, env, error::Error, fmt::Display, path::PathBuf};
5use tera::{Context, Error as TeraError, ErrorKind, Function, Tera, Value};
6use tokio::{fs, sync::RwLock};
7
8#[derive(Debug)]
9pub enum TemplateError {
10 NotFound(String),
11 InvalidConfig(String),
12 Internal(String),
13 PathNotFound(PathBuf),
14}
15
16impl Display for TemplateError {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 TemplateError::InvalidConfig(msg) => write!(f, "Invalid configuration: {}", msg),
20 TemplateError::Internal(msg) => write!(f, "Internal template error: {}", msg),
21 TemplateError::NotFound(tmpl) => write!(f, "Template '{}' not found", tmpl),
22 TemplateError::PathNotFound(path) => {
23 write!(f, "Path '{}' not found or inaccessible", path.display())
24 }
25 }
26 }
27}
28
29impl Error for TemplateError {}
30
31pub struct Templater {
32 context: HashMap<String, FieldValue>,
33 root: PathBuf,
34 tera: RwLock<Tera>,
35}
36
37impl Templater {
38 pub fn new(
39 mut context: HashMap<String, FieldValue>,
40 root_path: PathBuf,
41 ) -> Result<Self, TemplateError> {
42 debug!("Templater::new: root = {}", root_path.display());
43
44 context
46 .entry("platform".to_string())
47 .or_insert_with(|| FieldValue::String(env::consts::OS.to_string()));
48 context
49 .entry("root".to_string())
50 .or_insert_with(|| FieldValue::String(root_path.display().to_string()));
51
52 context
53 .entry("arch".into())
54 .or_insert_with(|| FieldValue::String(env::consts::ARCH.to_string()));
55
56 let ninja_root = root_path
57 .parent()
58 .and_then(|p| p.parent())
59 .ok_or_else(|| {
60 TemplateError::InvalidConfig(format!(
61 "Failed to resolve ninja_root from root_path: {}",
62 root_path.display()
63 ))
64 })?
65 .to_path_buf();
66
67 context
68 .entry("ninja_root".into())
69 .or_insert_with(|| FieldValue::String(ninja_root.display().to_string()));
70
71 let username = whoami::username()
72 .map_err(|e| TemplateError::Internal(format!("Failed to get username: {}", e)))?;
73
74 context
75 .entry("user".into())
76 .or_insert_with(|| FieldValue::String(username));
77
78 debug!(
79 "Templater::new: context size after injection = {}",
80 context.len()
81 );
82
83 let pattern = root_path.join(".ninja").join("**/*.tmpl");
84 let pattern_str = pattern.to_string_lossy();
85
86 info!(
87 "Templater::new: compiling Tera templates with pattern '{}'",
88 pattern_str
89 );
90
91 let mut tera = Tera::new(&pattern_str).map_err(|e| {
92 error!(
93 "Templater::new: failed to compile templates (pattern '{}'): {}",
94 pattern_str, e
95 );
96 TemplateError::Internal(format!("Failed to compile templates: {}", e))
97 })?;
98
99 tera.register_function("path", PathFunction);
100
101 info!(
102 "Templater::new: initialized successfully (root = {}, pattern = {})",
103 root_path.display(),
104 pattern_str
105 );
106
107 Ok(Self {
108 context,
109 root: root_path,
110 tera: RwLock::new(tera),
111 })
112 }
113
114 fn to_tera_context(&self) -> Context {
115 let mut ctx = Context::new();
116 for (key, value) in &self.context {
117 ctx.insert(key, value);
118 }
119 ctx
120 }
121
122 async fn render_with_diagnostics(
123 &self,
124 name: &str,
125 template: &str,
126 ) -> Result<String, TemplateError> {
127 debug!(
128 "Templater::render_with_diagnostics: rendering template '{}' (len = {})",
129 name,
130 template.len()
131 );
132
133 let ctx = self.to_tera_context();
134 let mut tera_guard = self.tera.write().await;
135
136 match tera_guard.render_str(template, &ctx) {
137 Ok(output) => {
138 debug!(
139 "Templater::render_with_diagnostics: rendered '{}' (output len = {})",
140 name,
141 output.len()
142 );
143 Ok(output)
144 }
145 Err(err) => {
146 error!(
147 "Templater::render_with_diagnostics: error rendering '{}': {}",
148 name, err
149 );
150 Err(Self::diagnose_tera_error(err, name))
151 }
152 }
153 }
154
155 fn diagnose_tera_error(err: TeraError, name: &str) -> TemplateError {
156 let mut msg = format!("Error rendering template '{}': {}", name, err);
158
159 match &err.kind {
160 ErrorKind::TemplateNotFound(tmpl) => {
161 error!(
162 "Templater::diagnose_tera_error: template '{}' not found (referenced as '{}')",
163 tmpl, name
164 );
165 return TemplateError::NotFound(tmpl.clone());
166 }
167 other => {
168 msg.push_str(&format!(" ({:?})", other));
169 }
170 }
171
172 let mut source_opt = err.source();
174 let mut depth = 0usize;
175 while let Some(source) = source_opt {
176 depth += 1;
177 msg.push_str(&format!(" | cause[{depth}]: {}", source));
178 source_opt = source.source();
179 }
180
181 error!("Templater::diagnose_tera_error: {}", msg);
182 TemplateError::Internal(msg)
183 }
184
185 pub async fn parse_template(&self, template: &str) -> Result<String, TemplateError> {
186 debug!(
187 "Templater::parse_template: inline template (len = {})",
188 template.len()
189 );
190 let result = self.render_with_diagnostics("<inline>", template).await;
191
192 if let Err(err) = &result {
193 error!("Templater::parse_template: error: {}", err);
194 }
195
196 result
197 }
198
199 pub async fn generate_config(&self, config_path: PathBuf) -> Result<(), TemplateError> {
200 debug!(
201 "Templater::generate_config: target config path = {}",
202 config_path.display()
203 );
204
205 let template_path = self.root.join(".ninja").join("config.tmpl");
206 debug!(
207 "Templater::generate_config: template path = {}",
208 template_path.display()
209 );
210
211 let template_content = fs::read_to_string(&template_path).await.map_err(|e| {
212 error!(
213 "Templater::generate_config: failed to read template '{}': {}",
214 template_path.display(),
215 e
216 );
217 TemplateError::PathNotFound(template_path.clone())
218 })?;
219
220 debug!(
221 "Templater::generate_config: read template (len = {}) from '{}'",
222 template_content.len(),
223 template_path.display()
224 );
225
226 let rendered = self
227 .render_with_diagnostics("config.tmpl", &template_content)
228 .await?;
229
230 debug!(
231 "Templater::generate_config: writing rendered config (len = {}) to '{}'",
232 rendered.len(),
233 config_path.display()
234 );
235
236 fs::write(&config_path, rendered).await.map_err(|e| {
237 error!(
238 "Templater::generate_config: failed to write config '{}': {}",
239 config_path.display(),
240 e
241 );
242 TemplateError::PathNotFound(config_path.clone())
243 })?;
244
245 info!(
246 "Templater::generate_config: config generated at '{}'",
247 config_path.display()
248 );
249 Ok(())
250 }
251}
252
253#[allow(dead_code)]
254struct PathFunction;
255
256#[allow(dead_code)]
257fn path(args: &HashMap<String, Value>) -> Result<tera::Value> {
258 let root = args.get("root").and_then(|v| v.as_str()).unwrap_or("");
259
260 let os_sep = std::path::MAIN_SEPARATOR.to_string();
261
262 let sep = args.get("sep").and_then(|v| v.as_str()).unwrap_or(&os_sep);
263
264 let path = args
265 .get("path")
266 .ok_or_else(|| anyhow::anyhow!("path argument required"))?
267 .as_str()
268 .ok_or_else(|| anyhow::anyhow!("path must be a string"))?;
269
270 let mut parts: Vec<&str> = Vec::new();
271
272 if !root.is_empty() {
273 #[cfg(windows)]
274 {
275 root.split('\\')
276 .filter(|s| !s.is_empty())
277 .for_each(|s| parts.push(s));
278 }
279
280 #[cfg(not(windows))]
281 {
282 root.split('/')
283 .filter(|s| !s.is_empty())
284 .for_each(|s| parts.push(s));
285 }
286 }
287
288 parts.extend(path.split('/').filter(|s| !s.is_empty()));
289 Ok(tera::Value::String(parts.join(sep)))
290}
291
292impl Function for PathFunction {
293 fn call(&self, args: &HashMap<String, Value>) -> tera::Result<tera::Value> {
294 path(args).map_err(|e| TeraError::msg(e))
295 }
296}