1use anyhow::Result;
2use async_trait::async_trait;
3use std::collections::HashSet;
4use std::path::Path;
5use std::sync::Arc;
6
7use crate::assets::{
8 AssetManager, embedded::EmbeddedAssetManager, filesystem::FileSystemAssetManager,
9};
10use crate::storage::xdg::XdgDirectories;
11
12pub struct LayeredAssetManager {
14 layers: Vec<Arc<dyn AssetManager>>,
15}
16
17impl LayeredAssetManager {
18 pub fn new(layers: Vec<Arc<dyn AssetManager>>) -> Self {
20 Self { layers }
21 }
22
23 pub fn new_with_standard_layers(
28 project_root: Option<&Path>,
29 xdg_dirs: &XdgDirectories,
30 ) -> Self {
31 let mut layers: Vec<Arc<dyn AssetManager>> = Vec::new();
32
33 if let Some(root) = project_root {
35 let project_tsk_dir = root.join(".tsk");
36 if project_tsk_dir.exists() {
37 layers.push(Arc::new(FileSystemAssetManager::new(project_tsk_dir)));
38 }
39 }
40
41 let user_templates_dir = xdg_dirs.config_dir().join("templates");
43 if user_templates_dir.exists() {
44 layers.push(Arc::new(FileSystemAssetManager::new(user_templates_dir)));
45 }
46
47 layers.push(Arc::new(EmbeddedAssetManager));
49
50 Self::new(layers)
51 }
52}
53
54#[async_trait]
55impl AssetManager for LayeredAssetManager {
56 fn get_template(&self, template_type: &str) -> Result<String> {
57 for layer in &self.layers {
58 match layer.get_template(template_type) {
59 Ok(template) => return Ok(template),
60 Err(_) => continue,
61 }
62 }
63
64 Err(anyhow::anyhow!(
65 "Template '{}' not found in any layer",
66 template_type
67 ))
68 }
69
70 fn get_dockerfile(&self, dockerfile_name: &str) -> Result<Vec<u8>> {
71 for layer in &self.layers {
72 match layer.get_dockerfile(dockerfile_name) {
73 Ok(content) => return Ok(content),
74 Err(_) => continue,
75 }
76 }
77
78 Err(anyhow::anyhow!(
79 "Dockerfile '{}' not found",
80 dockerfile_name
81 ))
82 }
83
84 fn get_dockerfile_file(&self, dockerfile_name: &str, file_path: &str) -> Result<Vec<u8>> {
85 for layer in &self.layers {
86 match layer.get_dockerfile_file(dockerfile_name, file_path) {
87 Ok(content) => return Ok(content),
88 Err(_) => continue,
89 }
90 }
91
92 Err(anyhow::anyhow!(
93 "Dockerfile file '{}/{}' not found",
94 dockerfile_name,
95 file_path
96 ))
97 }
98
99 fn list_templates(&self) -> Vec<String> {
100 let mut templates = HashSet::new();
101
102 for layer in &self.layers {
103 for template in layer.list_templates() {
104 templates.insert(template);
105 }
106 }
107
108 let mut result: Vec<String> = templates.into_iter().collect();
109 result.sort();
110 result
111 }
112
113 fn list_dockerfiles(&self) -> Vec<String> {
114 let mut dockerfiles = HashSet::new();
115
116 for layer in &self.layers {
117 for dockerfile in layer.list_dockerfiles() {
118 dockerfiles.insert(dockerfile);
119 }
120 }
121
122 let mut result: Vec<String> = dockerfiles.into_iter().collect();
123 result.sort();
124 result
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::assets::{
132 AssetManager, filesystem::FileSystemAssetManager, layered::LayeredAssetManager,
133 };
134 use crate::context::AppContext;
135 use crate::storage::XdgDirectories;
136 use std::fs;
137 use tempfile::TempDir;
138
139 struct MockAssetManager {
140 templates: Vec<String>,
141 template_content: String,
142 }
143
144 impl MockAssetManager {
145 fn new(templates: Vec<String>, template_content: String) -> Self {
146 Self {
147 templates,
148 template_content,
149 }
150 }
151 }
152
153 #[async_trait]
154 impl AssetManager for MockAssetManager {
155 fn get_template(&self, template_type: &str) -> Result<String> {
156 if self.templates.contains(&template_type.to_string()) {
157 Ok(self.template_content.clone())
158 } else {
159 Err(anyhow::anyhow!("Template not found"))
160 }
161 }
162
163 fn get_dockerfile(&self, _dockerfile_name: &str) -> Result<Vec<u8>> {
164 Err(anyhow::anyhow!("Not implemented"))
165 }
166
167 fn get_dockerfile_file(&self, _dockerfile_name: &str, _file_path: &str) -> Result<Vec<u8>> {
168 Err(anyhow::anyhow!("Not implemented"))
169 }
170
171 fn list_templates(&self) -> Vec<String> {
172 self.templates.clone()
173 }
174
175 fn list_dockerfiles(&self) -> Vec<String> {
176 Vec::new()
177 }
178 }
179
180 #[test]
181 fn test_layered_get_template_priority() {
182 let layer1 = Arc::new(MockAssetManager::new(
183 vec!["feature".to_string()],
184 "Layer 1 content".to_string(),
185 ));
186 let layer2 = Arc::new(MockAssetManager::new(
187 vec!["feature".to_string(), "fix".to_string()],
188 "Layer 2 content".to_string(),
189 ));
190
191 let manager = LayeredAssetManager::new(vec![layer1, layer2]);
192
193 assert_eq!(manager.get_template("feature").unwrap(), "Layer 1 content");
195
196 assert_eq!(manager.get_template("fix").unwrap(), "Layer 2 content");
198
199 assert!(manager.get_template("nonexistent").is_err());
201 }
202
203 #[test]
204 fn test_layered_list_templates_aggregation() {
205 let layer1 = Arc::new(MockAssetManager::new(
206 vec!["feature".to_string(), "fix".to_string()],
207 "content".to_string(),
208 ));
209 let layer2 = Arc::new(MockAssetManager::new(
210 vec!["fix".to_string(), "doc".to_string()],
211 "content".to_string(),
212 ));
213
214 let manager = LayeredAssetManager::new(vec![layer1, layer2]);
215 let templates = manager.list_templates();
216
217 assert_eq!(templates, vec!["doc", "feature", "fix"]);
218 }
219
220 #[test]
221 fn test_standard_layers_with_project() {
222 let temp_dir = TempDir::new().unwrap();
223
224 let project_templates = temp_dir.path().join(".tsk").join("templates");
226 fs::create_dir_all(&project_templates).unwrap();
227 fs::write(project_templates.join("feature.md"), "Project template").unwrap();
228
229 let config_dir = temp_dir.path().join("config");
231 fs::create_dir_all(&config_dir).unwrap();
232
233 let xdg_dirs = XdgDirectories::new_with_paths(
234 temp_dir.path().to_path_buf(),
235 temp_dir.path().to_path_buf(),
236 config_dir,
237 temp_dir.path().to_path_buf(),
238 );
239
240 let manager =
241 LayeredAssetManager::new_with_standard_layers(Some(temp_dir.path()), &xdg_dirs);
242
243 assert_eq!(manager.get_template("feature").unwrap(), "Project template");
245 }
246
247 #[test]
248 fn test_layered_template_resolution_priority() {
249 let temp_dir = TempDir::new().unwrap();
251
252 let project_dir = temp_dir.path().join("project");
254 let project_templates = project_dir.join(".tsk").join("templates");
255 fs::create_dir_all(&project_templates).unwrap();
256 fs::write(project_templates.join("feat.md"), "Project feat template").unwrap();
257 fs::write(project_templates.join("fix.md"), "Project fix template").unwrap();
258
259 let config_dir = temp_dir.path().join("config");
261 let user_templates = config_dir.join("templates");
262 fs::create_dir_all(&user_templates).unwrap();
263 fs::write(user_templates.join("feat.md"), "User feat template").unwrap();
264 fs::write(user_templates.join("doc.md"), "User doc template").unwrap();
265
266 let xdg_dirs = XdgDirectories::new_with_paths(
268 temp_dir.path().join("data"),
269 temp_dir.path().join("runtime"),
270 config_dir,
271 temp_dir.path().join("cache"),
272 );
273
274 let manager = LayeredAssetManager::new_with_standard_layers(Some(&project_dir), &xdg_dirs);
276
277 assert_eq!(
279 manager.get_template("feat").unwrap(),
280 "Project feat template"
281 );
282 assert_eq!(manager.get_template("fix").unwrap(), "Project fix template");
283 assert_eq!(manager.get_template("doc").unwrap(), "User doc template");
284
285 assert!(
287 manager
288 .get_template("refactor")
289 .unwrap()
290 .contains("{{DESCRIPTION}}")
291 );
292
293 let all_templates = manager.list_templates();
295 assert!(all_templates.contains(&"feat".to_string()));
296 assert!(all_templates.contains(&"fix".to_string()));
297 assert!(all_templates.contains(&"doc".to_string()));
298 assert!(all_templates.contains(&"refactor".to_string()));
299 }
300
301 #[test]
302 fn test_filesystem_asset_manager_with_missing_directory() {
303 let temp_dir = TempDir::new().unwrap();
304 let nonexistent_dir = temp_dir.path().join("nonexistent");
305
306 let manager = FileSystemAssetManager::new(nonexistent_dir);
307
308 assert!(manager.get_template("feat").is_err());
310
311 assert!(manager.list_templates().is_empty());
313 }
314
315 #[test]
316 fn test_template_content_validation() {
317 let temp_dir = TempDir::new().unwrap();
318 let templates_dir = temp_dir.path().join("templates");
319 fs::create_dir_all(&templates_dir).unwrap();
320
321 let valid_content =
323 "# Feature Template\n\n{{DESCRIPTION}}\n\n## Best Practices\n- Test your code";
324 fs::write(templates_dir.join("valid.md"), valid_content).unwrap();
325
326 let manager = FileSystemAssetManager::new(templates_dir);
327 let template = manager.get_template("valid").unwrap();
328
329 assert!(template.contains("{{DESCRIPTION}}"));
331 assert!(template.contains("Best Practices"));
332 }
333
334 #[tokio::test]
335 async fn test_app_context_with_layered_asset_manager() {
336 let temp_dir = TempDir::new().unwrap();
338 let repo_dir = temp_dir.path().join("repo");
339 fs::create_dir_all(&repo_dir).unwrap();
340
341 std::process::Command::new("git")
343 .args(&["init"])
344 .current_dir(&repo_dir)
345 .output()
346 .expect("Failed to init git repo");
347
348 let project_templates = repo_dir.join(".tsk").join("templates");
350 fs::create_dir_all(&project_templates).unwrap();
351 fs::write(
352 project_templates.join("custom.md"),
353 "Custom project template",
354 )
355 .unwrap();
356
357 std::env::set_current_dir(&repo_dir).unwrap();
359
360 let app_context = AppContext::builder().build();
362
363 let asset_manager = LayeredAssetManager::new_with_standard_layers(
365 Some(&repo_dir),
366 &app_context.xdg_directories(),
367 );
368
369 let template = asset_manager.get_template("custom");
371 assert!(template.is_ok());
372 assert_eq!(template.unwrap(), "Custom project template");
373
374 assert!(asset_manager.get_template("feat").is_ok());
376 }
377
378 #[test]
379 fn test_template_listing_deduplication() {
380 let temp_dir = TempDir::new().unwrap();
381
382 let project_dir = temp_dir.path().join("project");
384 let project_templates = project_dir.join(".tsk").join("templates");
385 fs::create_dir_all(&project_templates).unwrap();
386 fs::write(project_templates.join("feat.md"), "Project feat").unwrap();
387
388 let config_dir = temp_dir.path().join("config");
390 let user_templates = config_dir.join("templates");
391 fs::create_dir_all(&user_templates).unwrap();
392 fs::write(user_templates.join("feat.md"), "User feat").unwrap();
393 fs::write(user_templates.join("custom.md"), "User custom").unwrap();
394
395 let xdg_dirs = XdgDirectories::new_with_paths(
396 temp_dir.path().join("data"),
397 temp_dir.path().join("runtime"),
398 config_dir,
399 temp_dir.path().join("cache"),
400 );
401
402 let manager = LayeredAssetManager::new_with_standard_layers(Some(&project_dir), &xdg_dirs);
403
404 let templates = manager.list_templates();
405
406 let feat_count = templates.iter().filter(|t| t == &"feat").count();
408 assert_eq!(feat_count, 1);
409
410 assert!(templates.contains(&"custom".to_string()));
412 }
413}