1use anyhow::{Context, Result};
2use glob::glob;
3use std::path::PathBuf;
4
5use crate::cache::{CacheManager, CachedPack};
6use crate::lockfile::LockfileManager;
7
8#[derive(Debug, Clone)]
10pub struct TemplateResolver {
11 cache_manager: CacheManager,
12 lockfile_manager: LockfileManager,
13}
14
15#[derive(Debug, Clone)]
17pub struct TemplateSource {
18 pub pack_id: String,
19 pub template_path: PathBuf,
20 pub pack: CachedPack,
21 pub manifest: Option<crate::rpack::RpackManifest>,
22}
23
24#[derive(Debug, Clone)]
26pub struct TemplateSearchResult {
27 pub pack_id: String,
28 pub template_path: PathBuf,
29 pub pack_name: String,
30 pub pack_description: String,
31}
32
33impl TemplateResolver {
34 pub fn new(cache_manager: CacheManager, lockfile_manager: LockfileManager) -> Self {
36 Self {
37 cache_manager,
38 lockfile_manager,
39 }
40 }
41
42 pub fn resolve(&self, template_ref: &str) -> Result<TemplateSource> {
44 let (pack_id, template_path) = self.parse_template_ref(template_ref)?;
45
46 let lock_entry = self
48 .lockfile_manager
49 .get(&pack_id)?
50 .with_context(|| format!("Pack '{}' not found in lockfile", pack_id))?;
51
52 let cached_pack = self
54 .cache_manager
55 .load_cached(&pack_id, &lock_entry.version)
56 .with_context(|| format!("Pack '{}' not found in cache", pack_id))?;
57
58 let full_template_path = self.resolve_template_path(&cached_pack, &template_path)?;
60
61 if !full_template_path.exists() {
63 anyhow::bail!(
64 "Template '{}' not found in pack '{}'",
65 template_path,
66 pack_id
67 );
68 }
69
70 let manifest = cached_pack.manifest.clone();
71
72 Ok(TemplateSource {
73 pack_id,
74 template_path: full_template_path,
75 pack: cached_pack,
76 manifest,
77 })
78 }
79
80 fn parse_template_ref(&self, template_ref: &str) -> Result<(String, String)> {
82 let parts: Vec<&str> = template_ref.split(':').collect();
83
84 if parts.len() != 2 {
85 anyhow::bail!(
86 "Invalid template reference format: '{}'. Expected 'pack_id:template_path'",
87 template_ref
88 );
89 }
90
91 let pack_id = parts[0].to_string();
92 let template_path = parts[1].to_string();
93
94 if pack_id.is_empty() {
95 anyhow::bail!("Empty pack ID in template reference: '{}'", template_ref);
96 }
97
98 if template_path.is_empty() {
99 anyhow::bail!(
100 "Empty template path in template reference: '{}'",
101 template_ref
102 );
103 }
104
105 Ok((pack_id, template_path))
106 }
107
108 fn resolve_template_path(
110 &self, cached_pack: &CachedPack, template_path: &str,
111 ) -> Result<PathBuf> {
112 let mut full_path = cached_pack.path.join("templates");
114
115 for component in template_path.split('/') {
117 if component == ".." {
118 anyhow::bail!("Template path cannot contain '..': {}", template_path);
119 }
120 if component.is_empty() {
121 continue;
122 }
123 full_path = full_path.join(component);
124 }
125
126 Ok(full_path)
127 }
128
129 pub fn search_templates(&self, query: Option<&str>) -> Result<Vec<TemplateSearchResult>> {
131 let installed_packs = self.lockfile_manager.installed_packs()?;
132 let mut results = Vec::new();
133
134 for (pack_id, lock_entry) in installed_packs {
135 if let Ok(cached_pack) = self
136 .cache_manager
137 .load_cached(&pack_id, &lock_entry.version)
138 {
139 let pack_templates = self.find_templates_in_pack(&cached_pack)?;
140
141 for template_path in pack_templates {
142 let template_name = template_path
143 .file_name()
144 .and_then(|n| n.to_str())
145 .unwrap_or("unknown");
146
147 if let Some(query) = query {
149 let query_lower = query.to_lowercase();
150 if !template_name.to_lowercase().contains(&query_lower) {
151 continue;
152 }
153 }
154
155 results.push(TemplateSearchResult {
156 pack_id: pack_id.clone(),
157 template_path: template_path.clone(),
158 pack_name: cached_pack
159 .manifest
160 .as_ref()
161 .map(|m| m.metadata.name.clone())
162 .unwrap_or_else(|| pack_id.clone()),
163 pack_description: cached_pack
164 .manifest
165 .as_ref()
166 .map(|m| m.metadata.description.clone())
167 .unwrap_or_else(|| "No description".to_string()),
168 });
169 }
170 }
171 }
172
173 results.sort_by(|a, b| {
175 a.pack_name
176 .cmp(&b.pack_name)
177 .then_with(|| a.template_path.cmp(&b.template_path))
178 });
179
180 Ok(results)
181 }
182
183 fn find_templates_in_pack(&self, cached_pack: &CachedPack) -> Result<Vec<PathBuf>> {
185 if let Some(manifest) = &cached_pack.manifest {
186 manifest.discover_templates(&cached_pack.path)
187 } else {
188 let conventions = crate::rpack::PackConventions::default();
190 let mut templates = Vec::new();
191
192 for pattern in conventions.template_patterns {
193 let full_pattern = cached_pack.path.join(pattern);
194 for entry in glob(&full_pattern.to_string_lossy())? {
195 templates.push(entry?);
196 }
197 }
198
199 templates.sort();
200 Ok(templates)
201 }
202 }
203
204 pub fn get_pack_templates(&self, pack_id: &str) -> Result<Vec<String>> {
206 let lock_entry = self
207 .lockfile_manager
208 .get(pack_id)?
209 .with_context(|| format!("Pack '{}' not found in lockfile", pack_id))?;
210
211 let cached_pack = self
212 .cache_manager
213 .load_cached(pack_id, &lock_entry.version)
214 .with_context(|| format!("Pack '{}' not found in cache", pack_id))?;
215
216 let templates = self.find_templates_in_pack(&cached_pack)?;
218 let templates_dir = cached_pack.path.join("templates");
219 let mut template_paths = Vec::new();
220
221 for template_path in templates {
222 let relative_path = template_path
224 .strip_prefix(&templates_dir)
225 .context("Failed to get relative template path")?;
226
227 template_paths.push(relative_path.to_string_lossy().to_string());
228 }
229
230 template_paths.sort();
232
233 Ok(template_paths)
234 }
235
236 pub fn get_template_info(&self, template_ref: &str) -> Result<TemplateInfo> {
238 let template_source = self.resolve(template_ref)?;
239
240 let content = std::fs::read_to_string(&template_source.template_path)
242 .context("Failed to read template file")?;
243
244 let (frontmatter, template_content) = self.parse_frontmatter(&content)?;
246
247 Ok(TemplateInfo {
248 pack_id: template_source.pack_id,
249 template_path: template_source.template_path,
250 frontmatter,
251 content: template_content,
252 pack_info: template_source.manifest.map(|m| m.metadata),
253 })
254 }
255
256 fn parse_frontmatter(&self, content: &str) -> Result<(Option<serde_yaml::Value>, String)> {
258 use gray_matter::Matter;
259
260 let matter = Matter::<gray_matter::engine::YAML>::new();
261 let parsed = matter.parse(content)?;
262
263 let frontmatter = parsed.data.map(|data: serde_yaml::Value| data);
264 let content = parsed.content;
265
266 Ok((frontmatter, content))
267 }
268}
269
270#[derive(Debug, Clone)]
272pub struct TemplateInfo {
273 pub pack_id: String,
274 pub template_path: PathBuf,
275 pub frontmatter: Option<serde_yaml::Value>,
276 pub content: String,
277 pub pack_info: Option<crate::rpack::RpackMetadata>,
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use std::fs;
284 use tempfile::TempDir;
285
286 #[test]
287 fn test_parse_template_ref() {
288 let temp_dir = TempDir::new().unwrap();
289 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
290 let lockfile_manager = LockfileManager::new(temp_dir.path());
291 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
292
293 let (pack_id, template_path) = resolver
294 .parse_template_ref("io.rgen.test:main.tmpl")
295 .unwrap();
296 assert_eq!(pack_id, "io.rgen.test");
297 assert_eq!(template_path, "main.tmpl");
298 }
299
300 #[test]
301 fn test_parse_template_ref_invalid() {
302 let temp_dir = TempDir::new().unwrap();
303 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
304 let lockfile_manager = LockfileManager::new(temp_dir.path());
305 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
306
307 assert!(resolver.parse_template_ref("invalid").is_err());
309
310 assert!(resolver.parse_template_ref(":template.tmpl").is_err());
312
313 assert!(resolver.parse_template_ref("pack:").is_err());
315 }
316
317 #[test]
318 fn test_resolve_template_path() {
319 let temp_dir = TempDir::new().unwrap();
320 let pack_dir = temp_dir.path().join("pack");
321 let templates_dir = pack_dir.join("templates");
322 fs::create_dir_all(&templates_dir).unwrap();
323
324 let cached_pack = CachedPack {
325 id: "io.rgen.test".to_string(),
326 version: "1.0.0".to_string(),
327 path: pack_dir,
328 sha256: "abc123".to_string(),
329 manifest: None,
330 };
331
332 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
333 let lockfile_manager = LockfileManager::new(temp_dir.path());
334 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
335
336 let resolved_path = resolver
337 .resolve_template_path(&cached_pack, "main.tmpl")
338 .unwrap();
339 assert_eq!(resolved_path, templates_dir.join("main.tmpl"));
340 }
341
342 #[test]
343 fn test_resolve_template_path_security() {
344 let temp_dir = TempDir::new().unwrap();
345 let pack_dir = temp_dir.path().join("pack");
346 let cached_pack = CachedPack {
347 id: "io.rgen.test".to_string(),
348 version: "1.0.0".to_string(),
349 path: pack_dir,
350 sha256: "abc123".to_string(),
351 manifest: None,
352 };
353
354 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
355 let lockfile_manager = LockfileManager::new(temp_dir.path());
356 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
357
358 assert!(resolver
360 .resolve_template_path(&cached_pack, "../outside.tmpl")
361 .is_err());
362 }
363
364 #[test]
365 fn test_template_resolver_new() {
366 let temp_dir = TempDir::new().unwrap();
367 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
368 let lockfile_manager = LockfileManager::new(temp_dir.path());
369
370 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
371
372 assert!(resolver.cache_manager.cache_dir().exists());
374 }
375
376 #[test]
377 fn test_resolve_template_path_nested() {
378 let temp_dir = TempDir::new().unwrap();
379 let pack_dir = temp_dir.path().join("pack");
380 let templates_dir = pack_dir.join("templates");
381 fs::create_dir_all(&templates_dir).unwrap();
382
383 let cached_pack = CachedPack {
384 id: "io.rgen.test".to_string(),
385 version: "1.0.0".to_string(),
386 path: pack_dir,
387 sha256: "abc123".to_string(),
388 manifest: None,
389 };
390
391 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
392 let lockfile_manager = LockfileManager::new(temp_dir.path());
393 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
394
395 let resolved_path = resolver
396 .resolve_template_path(&cached_pack, "nested/sub.tmpl")
397 .unwrap();
398 assert_eq!(resolved_path, templates_dir.join("nested").join("sub.tmpl"));
399 }
400
401 #[test]
402 fn test_resolve_template_path_empty_components() {
403 let temp_dir = TempDir::new().unwrap();
404 let pack_dir = temp_dir.path().join("pack");
405 let templates_dir = pack_dir.join("templates");
406 fs::create_dir_all(&templates_dir).unwrap();
407
408 let cached_pack = CachedPack {
409 id: "io.rgen.test".to_string(),
410 version: "1.0.0".to_string(),
411 path: pack_dir,
412 sha256: "abc123".to_string(),
413 manifest: None,
414 };
415
416 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
417 let lockfile_manager = LockfileManager::new(temp_dir.path());
418 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
419
420 let resolved_path = resolver
422 .resolve_template_path(&cached_pack, "a//b/")
423 .unwrap();
424 assert_eq!(resolved_path, templates_dir.join("a").join("b"));
425 }
426
427 #[test]
428 fn test_parse_frontmatter_basic() {
429 let temp_dir = TempDir::new().unwrap();
430 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
431 let lockfile_manager = LockfileManager::new(temp_dir.path());
432 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
433
434 let content = r#"---
435to: "output.txt"
436vars:
437 name: "Test"
438---
439Hello {{ name }}
440"#;
441
442 let (frontmatter, template_content) = resolver.parse_frontmatter(content).unwrap();
443
444 assert!(frontmatter.is_some());
445 assert!(template_content.contains("Hello {{ name }}"));
446 }
447
448 #[test]
449 fn test_parse_frontmatter_no_frontmatter() {
450 let temp_dir = TempDir::new().unwrap();
451 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
452 let lockfile_manager = LockfileManager::new(temp_dir.path());
453 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
454
455 let content = "Hello World";
456
457 let (frontmatter, template_content) = resolver.parse_frontmatter(content).unwrap();
458
459 assert!(frontmatter.is_none());
460 assert_eq!(template_content, "Hello World");
461 }
462
463 #[test]
464 fn test_find_templates_in_pack_with_manifest() {
465 let temp_dir = TempDir::new().unwrap();
466 let pack_dir = temp_dir.path().join("pack");
467 let templates_dir = pack_dir.join("templates");
468 fs::create_dir_all(&templates_dir).unwrap();
469
470 fs::write(templates_dir.join("main.tmpl"), "template1").unwrap();
472 fs::write(templates_dir.join("sub.tmpl"), "template2").unwrap();
473
474 let manifest = crate::rpack::RpackManifest {
475 metadata: crate::rpack::RpackMetadata {
476 id: "io.rgen.test".to_string(),
477 name: "test-pack".to_string(),
478 version: "1.0.0".to_string(),
479 description: "Test pack".to_string(),
480 license: "MIT".to_string(),
481 rgen_compat: "1.0.0".to_string(),
482 },
483 dependencies: std::collections::BTreeMap::new(),
484 templates: crate::rpack::TemplatesConfig {
485 patterns: vec![
486 "templates/main.tmpl".to_string(),
487 "templates/sub.tmpl".to_string(),
488 ],
489 includes: vec![],
490 },
491 macros: crate::rpack::MacrosConfig::default(),
492 rdf: crate::rpack::RdfConfig {
493 base: None,
494 prefixes: std::collections::BTreeMap::new(),
495 patterns: vec![],
496 inline: vec![],
497 },
498 queries: crate::rpack::QueriesConfig::default(),
499 shapes: crate::rpack::ShapesConfig::default(),
500 preset: crate::rpack::PresetConfig::default(),
501 };
502
503 let cached_pack = CachedPack {
504 id: "io.rgen.test".to_string(),
505 version: "1.0.0".to_string(),
506 path: pack_dir,
507 sha256: "abc123".to_string(),
508 manifest: Some(manifest),
509 };
510
511 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
512 let lockfile_manager = LockfileManager::new(temp_dir.path());
513 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
514
515 let templates = resolver.find_templates_in_pack(&cached_pack).unwrap();
516
517 assert_eq!(templates.len(), 2);
518 assert!(templates.iter().any(|t| t.ends_with("main.tmpl")));
519 assert!(templates.iter().any(|t| t.ends_with("sub.tmpl")));
520 }
521
522 #[test]
523 fn test_find_templates_in_pack_without_manifest() {
524 let temp_dir = TempDir::new().unwrap();
525 let pack_dir = temp_dir.path().join("pack");
526 let templates_dir = pack_dir.join("templates");
527 fs::create_dir_all(&templates_dir).unwrap();
528
529 fs::write(templates_dir.join("main.tmpl"), "template1").unwrap();
531 fs::write(templates_dir.join("sub.tmpl"), "template2").unwrap();
532
533 let cached_pack = CachedPack {
534 id: "io.rgen.test".to_string(),
535 version: "1.0.0".to_string(),
536 path: pack_dir,
537 sha256: "abc123".to_string(),
538 manifest: None,
539 };
540
541 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
542 let lockfile_manager = LockfileManager::new(temp_dir.path());
543 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
544
545 let templates = resolver.find_templates_in_pack(&cached_pack).unwrap();
546
547 assert_eq!(templates.len(), 2);
548 assert!(templates.iter().any(|t| t.ends_with("main.tmpl")));
549 assert!(templates.iter().any(|t| t.ends_with("sub.tmpl")));
550 }
551
552 #[test]
553 fn test_search_templates_empty() {
554 let temp_dir = TempDir::new().unwrap();
555 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
556 let lockfile_manager = LockfileManager::new(temp_dir.path());
557 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
558
559 let results = resolver.search_templates(None).unwrap();
560 assert!(results.is_empty());
561 }
562
563 #[test]
564 fn test_get_pack_templates_nonexistent_pack() {
565 let temp_dir = TempDir::new().unwrap();
566 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
567 let lockfile_manager = LockfileManager::new(temp_dir.path());
568 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
569
570 assert!(resolver.get_pack_templates("nonexistent.pack").is_err());
572 }
573}