ggen_core/
resolver.rs

1//! Template resolver for gpack:template syntax
2//!
3//! This module provides functionality to resolve template references in the
4//! format `pack_id:template_path` to actual template files in cached packs.
5//! It integrates with the cache manager and lockfile to locate and load templates.
6//!
7//! ## Template Reference Format
8//!
9//! Templates are referenced using the format: `pack_id:template_path`
10//!
11//! - `pack_id`: The identifier of the gpack (e.g., `io.ggen.rust.api`)
12//! - `template_path`: Relative path from the pack's templates directory (e.g., `main.tmpl`)
13//!
14//! ## Features
15//!
16//! - **Template Resolution**: Resolve `pack_id:template_path` to file system paths
17//! - **Template Discovery**: Find all templates in installed packs
18//! - **Template Search**: Search templates by name across all packs
19//! - **Security**: Prevent path traversal attacks
20//! - **Manifest Integration**: Use gpack manifests for template discovery
21//!
22//! ## Examples
23//!
24//! ### Resolving a Template
25//!
26//! ```rust,no_run
27//! use ggen_core::resolver::TemplateResolver;
28//! use ggen_core::cache::CacheManager;
29//! use ggen_core::lockfile::LockfileManager;
30//! use std::path::Path;
31//!
32//! # fn main() -> ggen_utils::error::Result<()> {
33//! let cache = CacheManager::new()?;
34//! let lockfile = LockfileManager::new(Path::new("."));
35//! let resolver = TemplateResolver::new(cache, lockfile);
36//!
37//! // Resolve template reference
38//! let source = resolver.resolve("io.ggen.rust.api:main.tmpl")?;
39//! println!("Template at: {:?}", source.template_path);
40//! # Ok(())
41//! # }
42//! ```
43//!
44//! ### Searching Templates
45//!
46//! ```rust,no_run
47//! use ggen_core::resolver::TemplateResolver;
48//! use ggen_core::cache::CacheManager;
49//! use ggen_core::lockfile::LockfileManager;
50//! use std::path::Path;
51//!
52//! # fn main() -> ggen_utils::error::Result<()> {
53//! let cache = CacheManager::new()?;
54//! let lockfile = LockfileManager::new(Path::new("."));
55//! let resolver = TemplateResolver::new(cache, lockfile);
56//!
57//! // Search for templates containing "api"
58//! let results = resolver.search_templates(Some("api"))?;
59//! for result in results {
60//!     println!("Found: {}:{}", result.pack_id, result.template_path.display());
61//! }
62//! # Ok(())
63//! # }
64//! ```
65//!
66//! ### Getting Template Information
67//!
68//! ```rust,no_run
69//! use ggen_core::resolver::TemplateResolver;
70//! use ggen_core::cache::CacheManager;
71//! use ggen_core::lockfile::LockfileManager;
72//! use std::path::Path;
73//!
74//! # fn main() -> ggen_utils::error::Result<()> {
75//! let cache = CacheManager::new()?;
76//! let lockfile = LockfileManager::new(Path::new("."));
77//! let resolver = TemplateResolver::new(cache, lockfile);
78//!
79//! // Get template info including frontmatter
80//! let info = resolver.get_template_info("io.ggen.rust.api:main.tmpl")?;
81//! println!("Template frontmatter: {:?}", info.frontmatter);
82//! # Ok(())
83//! # }
84//! ```
85
86use ggen_utils::error::{Error, Result};
87use glob::glob;
88use std::path::PathBuf;
89
90use crate::cache::{CacheManager, CachedPack};
91use crate::lockfile::LockfileManager;
92
93/// Template resolver for gpack:template syntax
94#[derive(Debug, Clone)]
95pub struct TemplateResolver {
96    cache_manager: CacheManager,
97    lockfile_manager: LockfileManager,
98}
99
100/// Resolved template source
101#[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/// Template search result
110#[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    /// Create a new template resolver
120    ///
121    /// # Examples
122    ///
123    /// ```rust,no_run
124    /// use ggen_core::resolver::TemplateResolver;
125    /// use ggen_core::cache::CacheManager;
126    /// use ggen_core::lockfile::LockfileManager;
127    /// use std::path::Path;
128    ///
129    /// # fn main() -> ggen_utils::error::Result<()> {
130    /// let cache = CacheManager::new()?;
131    /// let lockfile = LockfileManager::new(Path::new("."));
132    /// let resolver = TemplateResolver::new(cache, lockfile);
133    /// # Ok(())
134    /// # }
135    /// ```
136    pub fn new(cache_manager: CacheManager, lockfile_manager: LockfileManager) -> Self {
137        Self {
138            cache_manager,
139            lockfile_manager,
140        }
141    }
142
143    /// Resolve a template reference in the format "pack_id:template_path"
144    ///
145    /// # Errors
146    ///
147    /// Returns an error if:
148    /// - The template reference format is invalid (must be `pack_id:template_path`)
149    /// - The pack is not found in the lockfile
150    /// - The pack is not cached locally
151    /// - The template path doesn't exist in the pack
152    /// - Path traversal is detected (e.g., `../` in template path)
153    ///
154    /// # Examples
155    ///
156    /// ## Success case
157    ///
158    /// ```rust,no_run
159    /// use ggen_core::resolver::TemplateResolver;
160    /// use ggen_core::cache::CacheManager;
161    /// use ggen_core::lockfile::LockfileManager;
162    /// use std::path::Path;
163    ///
164    /// # fn main() -> ggen_utils::error::Result<()> {
165    /// let cache = CacheManager::new()?;
166    /// let lockfile = LockfileManager::new(Path::new("."));
167    /// let resolver = TemplateResolver::new(cache, lockfile);
168    ///
169    /// // Resolve template reference
170    /// let source = resolver.resolve("io.ggen.rust.api:main.tmpl")?;
171    /// println!("Template at: {:?}", source.template_path);
172    /// # Ok(())
173    /// # }
174    /// ```
175    ///
176    /// ## Error case - Invalid format
177    ///
178    /// ```rust,no_run
179    /// use ggen_core::resolver::TemplateResolver;
180    /// use ggen_core::cache::CacheManager;
181    /// use ggen_core::lockfile::LockfileManager;
182    /// use std::path::Path;
183    ///
184    /// # fn main() -> ggen_utils::error::Result<()> {
185    /// let cache = CacheManager::new()?;
186    /// let lockfile = LockfileManager::new(Path::new("."));
187    /// let resolver = TemplateResolver::new(cache, lockfile);
188    ///
189    /// // This will fail because the format is invalid (missing colon)
190    /// let result = resolver.resolve("invalid-format");
191    /// assert!(result.is_err());
192    /// # Ok(())
193    /// # }
194    /// ```
195    ///
196    /// ## Error case - Pack not found
197    ///
198    /// ```rust,no_run
199    /// use ggen_core::resolver::TemplateResolver;
200    /// use ggen_core::cache::CacheManager;
201    /// use ggen_core::lockfile::LockfileManager;
202    /// use std::path::Path;
203    ///
204    /// # fn main() -> ggen_utils::error::Result<()> {
205    /// let cache = CacheManager::new()?;
206    /// let lockfile = LockfileManager::new(Path::new("."));
207    /// let resolver = TemplateResolver::new(cache, lockfile);
208    ///
209    /// // This will fail because the pack is not in the lockfile
210    /// let result = resolver.resolve("nonexistent.pack:template.tmpl");
211    /// assert!(result.is_err());
212    /// # Ok(())
213    /// # }
214    /// ```
215    pub fn resolve(&self, template_ref: &str) -> Result<TemplateSource> {
216        let (pack_id, template_path) = self.parse_template_ref(template_ref)?;
217
218        // Get pack from lockfile
219        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        // Load cached pack
225        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        // Resolve template path
236        let full_template_path = self.resolve_template_path(&cached_pack, &template_path)?;
237
238        // Verify template exists
239        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    /// Parse a template reference into pack ID and template path
257    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    /// Resolve template path relative to pack directory
288    fn resolve_template_path(
289        &self, cached_pack: &CachedPack, template_path: &str,
290    ) -> Result<PathBuf> {
291        // Start with templates directory
292        let mut full_path = cached_pack.path.join("templates");
293
294        // Add template path components
295        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    /// Search for templates across all installed packs
312    ///
313    /// # Example
314    ///
315    /// ```rust,no_run
316    /// use ggen_core::resolver::TemplateResolver;
317    /// use ggen_core::cache::CacheManager;
318    /// use ggen_core::lockfile::LockfileManager;
319    /// use std::path::Path;
320    ///
321    /// # fn main() -> ggen_utils::error::Result<()> {
322    /// let cache = CacheManager::new()?;
323    /// let lockfile = LockfileManager::new(Path::new("."));
324    /// let resolver = TemplateResolver::new(cache, lockfile);
325    ///
326    /// // Search for templates containing "api"
327    /// let results = resolver.search_templates(Some("api"))?;
328    /// for result in results {
329    ///     println!("Found: {}:{}", result.pack_id, result.template_path.display());
330    /// }
331    /// # Ok(())
332    /// # }
333    /// ```
334    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                    // Filter by query if provided
352                    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        // Sort by pack name, then template name
378        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    /// Find all templates in a pack using manifest discovery
388    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            // Fallback to default convention if no manifest
393            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                // Explicit error conversion: glob errors don't implement From
399                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    /// Get available templates for a specific pack
413    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        // Use manifest discovery to find templates
430        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            // Get relative path from templates directory
436            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        // Sort for consistent output
444        template_paths.sort();
445
446        Ok(template_paths)
447    }
448
449    /// Get template information including frontmatter
450    pub fn get_template_info(&self, template_ref: &str) -> Result<TemplateInfo> {
451        let template_source = self.resolve(template_ref)?;
452
453        // Read template content
454        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        // Parse frontmatter if present
458        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    /// Parse frontmatter from template content
470    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        // Explicit error conversion: gray_matter errors don't implement From
475        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/// Template information
487#[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        // Invalid format
524        assert!(resolver.parse_template_ref("invalid").is_err());
525
526        // Empty pack ID
527        assert!(resolver.parse_template_ref(":template.tmpl").is_err());
528
529        // Empty template path
530        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        // Should reject path traversal
575        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        // Resolver should be created successfully
589        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        // Should handle empty path components
637        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        // Create template files
687        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        // Create template files
746        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        // Should fail for nonexistent pack
787        assert!(resolver.get_pack_templates("nonexistent.pack").is_err());
788    }
789}