rgen_cli_lib/cmds/
list.rs

1use rayon::prelude::*;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4use rgen_utils::error::Result;
5use walkdir::WalkDir;
6
7// Cache for template metadata to avoid re-parsing
8static mut TEMPLATE_CACHE: Option<Arc<std::collections::HashMap<PathBuf, TemplateInfo>>> = None;
9static CACHE_INIT: std::sync::Once = std::sync::Once::new();
10
11#[derive(Debug, Clone)]
12struct TemplateInfo {
13    relative_path: String,
14    output_path: Option<String>,
15    variables: Vec<(String, String)>,
16    rdf_files: usize,
17    sparql_queries: usize,
18}
19
20pub fn run() -> Result<()> {
21    let templates_dir = find_templates_directory()?;
22
23    println!("Available templates:");
24    println!("===================");
25
26    // Use cached template info if available
27    let template_infos = get_cached_template_infos(&templates_dir)?;
28
29    if template_infos.is_empty() {
30        println!("No templates found in {}", templates_dir.display());
31    } else {
32        // Display templates in parallel for better performance
33        template_infos.par_iter().for_each(|(_, info)| {
34            display_template_info_cached(info);
35        });
36
37        println!("\nTotal: {} template(s)", template_infos.len());
38    }
39
40    Ok(())
41}
42
43fn find_templates_directory() -> Result<PathBuf> {
44    // Look for templates directory in common locations
45    let possible_paths = [
46        PathBuf::from("templates"),
47        PathBuf::from("examples"),
48        PathBuf::from("../templates"),
49        PathBuf::from("../examples"),
50    ];
51
52    for path in &possible_paths {
53        if path.exists() && path.is_dir() {
54            return Ok(path.clone());
55        }
56    }
57
58    Err(rgen_utils::error::Error::new(
59        "No templates directory found. Please ensure you're in a project with templates.",
60    ))
61}
62
63fn get_cached_template_infos(
64    templates_dir: &Path,
65) -> Result<Arc<std::collections::HashMap<PathBuf, TemplateInfo>>> {
66    CACHE_INIT.call_once(|| unsafe {
67        TEMPLATE_CACHE = Some(Arc::new(std::collections::HashMap::new()));
68    });
69
70    unsafe {
71        if let Some(cache) = (*&raw const TEMPLATE_CACHE).as_ref() {
72            if !cache.is_empty() {
73                return Ok(cache.clone());
74            }
75        }
76    }
77
78    // Build cache by scanning templates directory
79    let mut template_infos = std::collections::HashMap::new();
80
81    // Collect all .tmpl files first
82    let template_paths: Vec<_> = WalkDir::new(templates_dir)
83        .follow_links(true)
84        .into_iter()
85        .filter_map(|e| e.ok())
86        .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "tmpl"))
87        .map(|entry| entry.path().to_path_buf())
88        .collect();
89
90    // Process templates in parallel
91    let parsed_infos: Vec<_> = template_paths
92        .par_iter()
93        .filter_map(|template_path| parse_template_info(template_path, templates_dir).ok())
94        .collect();
95
96    // Build the cache
97    for (path, info) in parsed_infos {
98        template_infos.insert(path, info);
99    }
100
101    let cache = Arc::new(template_infos);
102    unsafe {
103        TEMPLATE_CACHE = Some(cache.clone());
104    }
105
106    Ok(cache)
107}
108
109fn parse_template_info(template_path: &Path, base_dir: &Path) -> Result<(PathBuf, TemplateInfo)> {
110    let relative_path = template_path
111        .strip_prefix(base_dir)
112        .unwrap_or(template_path)
113        .to_string_lossy()
114        .to_string();
115
116    let content = std::fs::read_to_string(template_path)?;
117
118    if let Some(frontmatter) = extract_frontmatter(&content) {
119        let output_path = frontmatter
120            .get("to")
121            .and_then(|v| v.as_str())
122            .map(|s| s.to_string());
123
124        let variables: Vec<(String, String)> = frontmatter
125            .get("vars")
126            .and_then(|v| v.as_object())
127            .map(|vars_map| {
128                vars_map
129                    .iter()
130                    .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
131                    .collect()
132            })
133            .unwrap_or_default();
134
135        let rdf_files = frontmatter
136            .get("rdf")
137            .and_then(|v| v.as_array())
138            .map(|arr| arr.len())
139            .unwrap_or(0);
140
141        let sparql_queries = frontmatter
142            .get("sparql")
143            .and_then(|v| v.as_array())
144            .map(|arr| arr.len())
145            .unwrap_or(0);
146
147        Ok((
148            template_path.to_path_buf(),
149            TemplateInfo {
150                relative_path,
151                output_path,
152                variables,
153                rdf_files,
154                sparql_queries,
155            },
156        ))
157    } else {
158        Ok((
159            template_path.to_path_buf(),
160            TemplateInfo {
161                relative_path,
162                output_path: None,
163                variables: Vec::new(),
164                rdf_files: 0,
165                sparql_queries: 0,
166            },
167        ))
168    }
169}
170
171fn display_template_info_cached(info: &TemplateInfo) {
172    println!("\nšŸ“„ {}", info.relative_path);
173
174    if let Some(output) = &info.output_path {
175        println!("   Output: {}", output);
176    }
177
178    if !info.variables.is_empty() {
179        println!("   Variables:");
180        for (key, value) in &info.variables {
181            println!("     {}: {}", key, value);
182        }
183    }
184
185    if info.rdf_files > 0 {
186        println!("   RDF files: {}", info.rdf_files);
187    }
188
189    if info.sparql_queries > 0 {
190        println!("   SPARQL queries: {}", info.sparql_queries);
191    }
192}
193
194fn extract_frontmatter(content: &str) -> Option<serde_json::Value> {
195    // Look for YAML frontmatter between --- markers
196    if content.starts_with("---\n") {
197        if let Some(end_marker) = content[4..].find("\n---\n") {
198            let yaml_content = &content[4..4 + end_marker];
199            serde_yaml::from_str::<serde_json::Value>(yaml_content).ok()
200        } else {
201            None
202        }
203    } else {
204        None
205    }
206}