1use ggen_utils::error::{Error, Result};
87use glob::glob;
88use std::path::PathBuf;
89
90use crate::cache::{CacheManager, CachedPack};
91use crate::lockfile::LockfileManager;
92
93#[derive(Debug, Clone)]
95pub struct TemplateResolver {
96 cache_manager: CacheManager,
97 lockfile_manager: LockfileManager,
98}
99
100#[derive(Debug, Clone)]
102pub struct TemplateSource {
103 pub pack_id: String,
104 pub template_path: PathBuf,
105 pub pack: CachedPack,
106 pub manifest: Option<crate::gpack::GpackManifest>,
107}
108
109#[derive(Debug, Clone)]
111pub struct TemplateSearchResult {
112 pub pack_id: String,
113 pub template_path: PathBuf,
114 pub pack_name: String,
115 pub pack_description: String,
116}
117
118impl TemplateResolver {
119 pub fn new(cache_manager: CacheManager, lockfile_manager: LockfileManager) -> Self {
137 Self {
138 cache_manager,
139 lockfile_manager,
140 }
141 }
142
143 pub fn resolve(&self, template_ref: &str) -> Result<TemplateSource> {
216 let (pack_id, template_path) = self.parse_template_ref(template_ref)?;
217
218 let lock_entry = self
220 .lockfile_manager
221 .get(&pack_id)?
222 .ok_or_else(|| Error::new(&format!("Pack '{}' not found in lockfile", pack_id)))?;
223
224 let cached_pack = self
226 .cache_manager
227 .load_cached(&pack_id, &lock_entry.version)
228 .map_err(|e| {
229 Error::with_context(
230 &format!("Pack '{}' not found in cache", pack_id),
231 &e.to_string(),
232 )
233 })?;
234
235 let full_template_path = self.resolve_template_path(&cached_pack, &template_path)?;
237
238 if !full_template_path.exists() {
240 return Err(ggen_utils::error::Error::new(&format!(
241 "Template '{}' not found in pack '{}'",
242 template_path, pack_id
243 )));
244 }
245
246 let manifest = cached_pack.manifest.clone();
247
248 Ok(TemplateSource {
249 pack_id,
250 template_path: full_template_path,
251 pack: cached_pack,
252 manifest,
253 })
254 }
255
256 fn parse_template_ref(&self, template_ref: &str) -> Result<(String, String)> {
258 let parts: Vec<&str> = template_ref.split(':').collect();
259
260 if parts.len() != 2 {
261 return Err(ggen_utils::error::Error::new(&format!(
262 "Invalid template reference format: '{}'. Expected 'pack_id:template_path'",
263 template_ref
264 )));
265 }
266
267 let pack_id = parts[0].to_string();
268 let template_path = parts[1].to_string();
269
270 if pack_id.is_empty() {
271 return Err(ggen_utils::error::Error::new(&format!(
272 "Empty pack ID in template reference: '{}'",
273 template_ref
274 )));
275 }
276
277 if template_path.is_empty() {
278 return Err(ggen_utils::error::Error::new(&format!(
279 "Empty template path in template reference: '{}'",
280 template_ref
281 )));
282 }
283
284 Ok((pack_id, template_path))
285 }
286
287 fn resolve_template_path(
289 &self, cached_pack: &CachedPack, template_path: &str,
290 ) -> Result<PathBuf> {
291 let mut full_path = cached_pack.path.join("templates");
293
294 for component in template_path.split('/') {
296 if component == ".." {
297 return Err(ggen_utils::error::Error::new(&format!(
298 "Template path cannot contain '..': {}",
299 template_path
300 )));
301 }
302 if component.is_empty() {
303 continue;
304 }
305 full_path = full_path.join(component);
306 }
307
308 Ok(full_path)
309 }
310
311 pub fn search_templates(&self, query: Option<&str>) -> Result<Vec<TemplateSearchResult>> {
335 let installed_packs = self.lockfile_manager.installed_packs()?;
336 let mut results = Vec::new();
337
338 for (pack_id, lock_entry) in installed_packs {
339 if let Ok(cached_pack) = self
340 .cache_manager
341 .load_cached(&pack_id, &lock_entry.version)
342 {
343 let pack_templates = self.find_templates_in_pack(&cached_pack)?;
344
345 for template_path in pack_templates {
346 let template_name = template_path
347 .file_name()
348 .and_then(|n| n.to_str())
349 .unwrap_or("unknown");
350
351 if let Some(query) = query {
353 let query_lower = query.to_lowercase();
354 if !template_name.to_lowercase().contains(&query_lower) {
355 continue;
356 }
357 }
358
359 results.push(TemplateSearchResult {
360 pack_id: pack_id.clone(),
361 template_path: template_path.clone(),
362 pack_name: cached_pack
363 .manifest
364 .as_ref()
365 .map(|m| m.metadata.name.clone())
366 .unwrap_or_else(|| pack_id.clone()),
367 pack_description: cached_pack
368 .manifest
369 .as_ref()
370 .map(|m| m.metadata.description.clone())
371 .unwrap_or_else(|| "No description".to_string()),
372 });
373 }
374 }
375 }
376
377 results.sort_by(|a, b| {
379 a.pack_name
380 .cmp(&b.pack_name)
381 .then_with(|| a.template_path.cmp(&b.template_path))
382 });
383
384 Ok(results)
385 }
386
387 fn find_templates_in_pack(&self, cached_pack: &CachedPack) -> Result<Vec<PathBuf>> {
389 if let Some(manifest) = &cached_pack.manifest {
390 manifest.discover_templates(&cached_pack.path)
391 } else {
392 let conventions = crate::gpack::PackConventions::default();
394 let mut templates = Vec::new();
395
396 for pattern in conventions.template_patterns {
397 let full_pattern = cached_pack.path.join(pattern);
398 for entry in glob(&full_pattern.to_string_lossy())
400 .map_err(|e| Error::new(&format!("Glob pattern error: {}", e)))?
401 {
402 templates
403 .push(entry.map_err(|e| Error::new(&format!("Glob entry error: {}", e)))?);
404 }
405 }
406
407 templates.sort();
408 Ok(templates)
409 }
410 }
411
412 pub fn get_pack_templates(&self, pack_id: &str) -> Result<Vec<String>> {
414 let lock_entry = self
415 .lockfile_manager
416 .get(pack_id)?
417 .ok_or_else(|| Error::new(&format!("Pack '{}' not found in lockfile", pack_id)))?;
418
419 let cached_pack = self
420 .cache_manager
421 .load_cached(pack_id, &lock_entry.version)
422 .map_err(|e| {
423 Error::with_context(
424 &format!("Pack '{}' not found in cache", pack_id),
425 &e.to_string(),
426 )
427 })?;
428
429 let templates = self.find_templates_in_pack(&cached_pack)?;
431 let templates_dir = cached_pack.path.join("templates");
432 let mut template_paths = Vec::new();
433
434 for template_path in templates {
435 let relative_path = template_path.strip_prefix(&templates_dir).map_err(|e| {
437 Error::with_context("Failed to get relative template path", &e.to_string())
438 })?;
439
440 template_paths.push(relative_path.to_string_lossy().to_string());
441 }
442
443 template_paths.sort();
445
446 Ok(template_paths)
447 }
448
449 pub fn get_template_info(&self, template_ref: &str) -> Result<TemplateInfo> {
451 let template_source = self.resolve(template_ref)?;
452
453 let content = std::fs::read_to_string(&template_source.template_path)
455 .map_err(|e| Error::with_context("Failed to read template file", &e.to_string()))?;
456
457 let (frontmatter, template_content) = self.parse_frontmatter(&content)?;
459
460 Ok(TemplateInfo {
461 pack_id: template_source.pack_id,
462 template_path: template_source.template_path,
463 frontmatter,
464 content: template_content,
465 pack_info: template_source.manifest.map(|m| m.metadata),
466 })
467 }
468
469 fn parse_frontmatter(&self, content: &str) -> Result<(Option<serde_yaml::Value>, String)> {
471 use gray_matter::Matter;
472
473 let matter = Matter::<gray_matter::engine::YAML>::new();
474 let parsed = matter
476 .parse(content)
477 .map_err(|e| Error::new(&format!("Failed to parse frontmatter: {}", e)))?;
478
479 let frontmatter = parsed.data.map(|data: serde_yaml::Value| data);
480 let content = parsed.content;
481
482 Ok((frontmatter, content))
483 }
484}
485
486#[derive(Debug, Clone)]
488pub struct TemplateInfo {
489 pub pack_id: String,
490 pub template_path: PathBuf,
491 pub frontmatter: Option<serde_yaml::Value>,
492 pub content: String,
493 pub pack_info: Option<crate::gpack::GpackMetadata>,
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use std::fs;
500 use tempfile::TempDir;
501
502 #[test]
503 fn test_parse_template_ref() {
504 let temp_dir = TempDir::new().unwrap();
505 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
506 let lockfile_manager = LockfileManager::new(temp_dir.path());
507 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
508
509 let (pack_id, template_path) = resolver
510 .parse_template_ref("io.ggen.test:main.tmpl")
511 .unwrap();
512 assert_eq!(pack_id, "io.ggen.test");
513 assert_eq!(template_path, "main.tmpl");
514 }
515
516 #[test]
517 fn test_parse_template_ref_invalid() {
518 let temp_dir = TempDir::new().unwrap();
519 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
520 let lockfile_manager = LockfileManager::new(temp_dir.path());
521 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
522
523 assert!(resolver.parse_template_ref("invalid").is_err());
525
526 assert!(resolver.parse_template_ref(":template.tmpl").is_err());
528
529 assert!(resolver.parse_template_ref("pack:").is_err());
531 }
532
533 #[test]
534 fn test_resolve_template_path() {
535 let temp_dir = TempDir::new().unwrap();
536 let pack_dir = temp_dir.path().join("pack");
537 let templates_dir = pack_dir.join("templates");
538 fs::create_dir_all(&templates_dir).unwrap();
539
540 let cached_pack = CachedPack {
541 id: "io.ggen.test".to_string(),
542 version: "1.0.0".to_string(),
543 path: pack_dir,
544 sha256: "abc123".to_string(),
545 manifest: None,
546 };
547
548 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
549 let lockfile_manager = LockfileManager::new(temp_dir.path());
550 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
551
552 let resolved_path = resolver
553 .resolve_template_path(&cached_pack, "main.tmpl")
554 .unwrap();
555 assert_eq!(resolved_path, templates_dir.join("main.tmpl"));
556 }
557
558 #[test]
559 fn test_resolve_template_path_security() {
560 let temp_dir = TempDir::new().unwrap();
561 let pack_dir = temp_dir.path().join("pack");
562 let cached_pack = CachedPack {
563 id: "io.ggen.test".to_string(),
564 version: "1.0.0".to_string(),
565 path: pack_dir,
566 sha256: "abc123".to_string(),
567 manifest: None,
568 };
569
570 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
571 let lockfile_manager = LockfileManager::new(temp_dir.path());
572 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
573
574 assert!(resolver
576 .resolve_template_path(&cached_pack, "../outside.tmpl")
577 .is_err());
578 }
579
580 #[test]
581 fn test_template_resolver_new() {
582 let temp_dir = TempDir::new().unwrap();
583 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
584 let lockfile_manager = LockfileManager::new(temp_dir.path());
585
586 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
587
588 assert!(resolver.cache_manager.cache_dir().exists());
590 }
591
592 #[test]
593 fn test_resolve_template_path_nested() {
594 let temp_dir = TempDir::new().unwrap();
595 let pack_dir = temp_dir.path().join("pack");
596 let templates_dir = pack_dir.join("templates");
597 fs::create_dir_all(&templates_dir).unwrap();
598
599 let cached_pack = CachedPack {
600 id: "io.ggen.test".to_string(),
601 version: "1.0.0".to_string(),
602 path: pack_dir,
603 sha256: "abc123".to_string(),
604 manifest: None,
605 };
606
607 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
608 let lockfile_manager = LockfileManager::new(temp_dir.path());
609 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
610
611 let resolved_path = resolver
612 .resolve_template_path(&cached_pack, "nested/sub.tmpl")
613 .unwrap();
614 assert_eq!(resolved_path, templates_dir.join("nested").join("sub.tmpl"));
615 }
616
617 #[test]
618 fn test_resolve_template_path_empty_components() {
619 let temp_dir = TempDir::new().unwrap();
620 let pack_dir = temp_dir.path().join("pack");
621 let templates_dir = pack_dir.join("templates");
622 fs::create_dir_all(&templates_dir).unwrap();
623
624 let cached_pack = CachedPack {
625 id: "io.ggen.test".to_string(),
626 version: "1.0.0".to_string(),
627 path: pack_dir,
628 sha256: "abc123".to_string(),
629 manifest: None,
630 };
631
632 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
633 let lockfile_manager = LockfileManager::new(temp_dir.path());
634 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
635
636 let resolved_path = resolver
638 .resolve_template_path(&cached_pack, "a//b/")
639 .unwrap();
640 assert_eq!(resolved_path, templates_dir.join("a").join("b"));
641 }
642
643 #[test]
644 fn test_parse_frontmatter_basic() {
645 let temp_dir = TempDir::new().unwrap();
646 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
647 let lockfile_manager = LockfileManager::new(temp_dir.path());
648 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
649
650 let content = r#"---
651to: "output.txt"
652vars:
653 name: "Test"
654---
655Hello {{ name }}
656"#;
657
658 let (frontmatter, template_content) = resolver.parse_frontmatter(content).unwrap();
659
660 assert!(frontmatter.is_some());
661 assert!(template_content.contains("Hello {{ name }}"));
662 }
663
664 #[test]
665 fn test_parse_frontmatter_no_frontmatter() {
666 let temp_dir = TempDir::new().unwrap();
667 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
668 let lockfile_manager = LockfileManager::new(temp_dir.path());
669 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
670
671 let content = "Hello World";
672
673 let (frontmatter, template_content) = resolver.parse_frontmatter(content).unwrap();
674
675 assert!(frontmatter.is_none());
676 assert_eq!(template_content, "Hello World");
677 }
678
679 #[test]
680 fn test_find_templates_in_pack_with_manifest() {
681 let temp_dir = TempDir::new().unwrap();
682 let pack_dir = temp_dir.path().join("pack");
683 let templates_dir = pack_dir.join("templates");
684 fs::create_dir_all(&templates_dir).unwrap();
685
686 fs::write(templates_dir.join("main.tmpl"), "template1").unwrap();
688 fs::write(templates_dir.join("sub.tmpl"), "template2").unwrap();
689
690 let manifest = crate::gpack::GpackManifest {
691 metadata: crate::gpack::GpackMetadata {
692 id: "io.ggen.test".to_string(),
693 name: "test-pack".to_string(),
694 version: "1.0.0".to_string(),
695 description: "Test pack".to_string(),
696 license: "MIT".to_string(),
697 ggen_compat: "1.0.0".to_string(),
698 },
699 dependencies: std::collections::BTreeMap::new(),
700 templates: crate::gpack::TemplatesConfig {
701 patterns: vec![
702 "templates/main.tmpl".to_string(),
703 "templates/sub.tmpl".to_string(),
704 ],
705 includes: vec![],
706 },
707 macros: crate::gpack::MacrosConfig::default(),
708 rdf: crate::gpack::RdfConfig {
709 base: None,
710 prefixes: std::collections::BTreeMap::new(),
711 patterns: vec![],
712 inline: vec![],
713 },
714 queries: crate::gpack::QueriesConfig::default(),
715 shapes: crate::gpack::ShapesConfig::default(),
716 preset: crate::gpack::PresetConfig::default(),
717 };
718
719 let cached_pack = CachedPack {
720 id: "io.ggen.test".to_string(),
721 version: "1.0.0".to_string(),
722 path: pack_dir,
723 sha256: "abc123".to_string(),
724 manifest: Some(manifest),
725 };
726
727 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
728 let lockfile_manager = LockfileManager::new(temp_dir.path());
729 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
730
731 let templates = resolver.find_templates_in_pack(&cached_pack).unwrap();
732
733 assert_eq!(templates.len(), 2);
734 assert!(templates.iter().any(|t| t.ends_with("main.tmpl")));
735 assert!(templates.iter().any(|t| t.ends_with("sub.tmpl")));
736 }
737
738 #[test]
739 fn test_find_templates_in_pack_without_manifest() {
740 let temp_dir = TempDir::new().unwrap();
741 let pack_dir = temp_dir.path().join("pack");
742 let templates_dir = pack_dir.join("templates");
743 fs::create_dir_all(&templates_dir).unwrap();
744
745 fs::write(templates_dir.join("main.tmpl"), "template1").unwrap();
747 fs::write(templates_dir.join("sub.tmpl"), "template2").unwrap();
748
749 let cached_pack = CachedPack {
750 id: "io.ggen.test".to_string(),
751 version: "1.0.0".to_string(),
752 path: pack_dir,
753 sha256: "abc123".to_string(),
754 manifest: None,
755 };
756
757 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
758 let lockfile_manager = LockfileManager::new(temp_dir.path());
759 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
760
761 let templates = resolver.find_templates_in_pack(&cached_pack).unwrap();
762
763 assert_eq!(templates.len(), 2);
764 assert!(templates.iter().any(|t| t.ends_with("main.tmpl")));
765 assert!(templates.iter().any(|t| t.ends_with("sub.tmpl")));
766 }
767
768 #[test]
769 fn test_search_templates_empty() {
770 let temp_dir = TempDir::new().unwrap();
771 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
772 let lockfile_manager = LockfileManager::new(temp_dir.path());
773 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
774
775 let results = resolver.search_templates(None).unwrap();
776 assert!(results.is_empty());
777 }
778
779 #[test]
780 fn test_get_pack_templates_nonexistent_pack() {
781 let temp_dir = TempDir::new().unwrap();
782 let cache_manager = CacheManager::with_dir(temp_dir.path().join("cache")).unwrap();
783 let lockfile_manager = LockfileManager::new(temp_dir.path());
784 let resolver = TemplateResolver::new(cache_manager, lockfile_manager);
785
786 assert!(resolver.get_pack_templates("nonexistent.pack").is_err());
788 }
789}