use std::{collections::HashSet, path::PathBuf}; use just_template::{Template, tmpl}; use regex::Regex; use tokio::fs; use crate::{ constants::{ COMMANDS_PATH, OVERRIDE_RENDERER_ENTRY, OVERRIDE_RENDERER_ENTRY_TEMPLATE, OVERRIDE_RENDERERS, OVERRIDE_RENDERERS_TEMPLATE, REGISTRY_TOML, }, resolve_types::resolve_type_paths, }; pub async fn generate_override_renderer(repo_root: &PathBuf) { let template_path = repo_root.join(OVERRIDE_RENDERER_ENTRY_TEMPLATE); let output_path = repo_root.join(OVERRIDE_RENDERER_ENTRY); let all_possible_types = collect_all_possible_types(&PathBuf::from(COMMANDS_PATH)).await; // Read the template let template_content = tokio::fs::read_to_string(&template_path).await.unwrap(); // Create template let mut template = Template::from(template_content); for type_name in &all_possible_types { let name = type_name.split("::").last().unwrap_or(type_name); tmpl!(template += { type_match_arms { (jv_output_type_name = name, jv_output_type = type_name) } }); } // Expand the template let final_content = template.expand().unwrap(); // Write the generated code tokio::fs::write(output_path, final_content).await.unwrap(); println!( "Generated override renderer entry with {} types using just_template", all_possible_types.len() ); } /// Generate override renderers list file from Registry.toml configuration using just_template pub async fn generate_override_renderers_list(repo_root: &PathBuf) { let template_path = repo_root.join(OVERRIDE_RENDERERS_TEMPLATE); let output_path = repo_root.join(OVERRIDE_RENDERERS); let config_path = repo_root.join(REGISTRY_TOML); // Read the template let template_content = tokio::fs::read_to_string(&template_path).await.unwrap(); // Read and parse the TOML configuration let config_content = tokio::fs::read_to_string(&config_path).await.unwrap(); let config: toml::Value = toml::from_str(&config_content).unwrap(); // Collect all renderer names let mut renderer_names = Vec::new(); let Some(table) = config.as_table() else { return; }; let Some(renderer_table) = table.get("renderer") else { return; }; let Some(renderer_table) = renderer_table.as_table() else { return; }; for (_, renderer_value) in renderer_table { let Some(renderer_config) = renderer_value.as_table() else { continue; }; let Some(name) = renderer_config.get("name").and_then(|v| v.as_str()) else { continue; }; renderer_names.push(name.to_string()); } // Create template let mut template = Template::from(template_content); for renderer_name in &renderer_names { tmpl!(template += { renderer { (renderer_name = renderer_name) } }); } let final_content = template.expand().unwrap(); // Write the generated code tokio::fs::write(output_path, final_content).await.unwrap(); println!( "Generated override renderers list with {} renderers using just_template", renderer_names.len() ); } pub async fn collect_all_possible_types(dir: &PathBuf) -> HashSet { let mut all_types = HashSet::new(); let mut dirs_to_visit = vec![dir.clone()]; while let Some(current_dir) = dirs_to_visit.pop() { let entries_result = fs::read_dir(¤t_dir).await; if entries_result.is_err() { continue; } let mut entries = entries_result.unwrap(); loop { let entry_result = entries.next_entry().await; if entry_result.is_err() { break; } let entry_opt = entry_result.unwrap(); if entry_opt.is_none() { break; } let entry = entry_opt.unwrap(); let path = entry.path(); if path.is_dir() { dirs_to_visit.push(path); continue; } let is_rs_file = path.extension().map(|ext| ext == "rs").unwrap_or(false); if !is_rs_file { continue; } let code_result = fs::read_to_string(&path).await; if code_result.is_err() { continue; } let code = code_result.unwrap(); let types_opt = resolve_type_paths(&code, get_output_types(&code).unwrap()); if let Some(types) = types_opt { for type_name in types { all_types.insert(type_name); } } } } all_types } pub fn get_output_types(code: &String) -> Option> { let mut output_types = Vec::new(); // Find all cmd_output! macros let cmd_output_re = Regex::new(r"cmd_output!\s*\(\s*([A-Za-z_][A-Za-z0-9_:]*)\s*=>").ok()?; for cap in cmd_output_re.captures_iter(code) { let type_name = cap[1].trim(); output_types.push(type_name.to_string()); } // Find all early_cmd_output! macros let early_cmd_output_re = Regex::new(r"early_cmd_output!\s*\(\s*([A-Za-z_][A-Za-z0-9_:]*)\s*=>").ok()?; for cap in early_cmd_output_re.captures_iter(code) { let type_name = cap[1].trim(); output_types.push(type_name.to_string()); } output_types.sort(); output_types.dedup(); Some(output_types) } #[cfg(test)] mod tests { use super::*; #[test] fn test_get_output_types() { const SITUATION: &str = " use crate::{ cmd_output, cmds::out::{ JVCustomOutput, JVCustomOutput2 }, systems::cmd::{ cmd_system::JVCommandContext, errors::{CmdExecuteError, CmdPrepareError}, workspace_reader::LocalWorkspaceReader, }, }; use cmd_system_macros::exec; use other::cmds::output::JVCustomOutputOutside; async fn exec() -> Result<(), CmdExecuteError> { early_cmd_output!(JVCustomOutput => output) early_cmd_output!(JVCustomOutput2 => { output }) cmd_output!(JVCustomOutputNotExist => output) cmd_output!(JVCustomOutputOutside => { output }) } "; let result = get_output_types(&SITUATION.to_string()); assert!(result.is_some(), "Parse failed"); let result = result.unwrap(); let expected = vec![ "JVCustomOutput".to_string(), "JVCustomOutput2".to_string(), "JVCustomOutputNotExist".to_string(), "JVCustomOutputOutside".to_string(), ]; assert_eq!(result, expected); let result = resolve_type_paths(&SITUATION.to_string(), expected); assert!(result.is_some(), "Parse failed"); let result = result.unwrap(); let expected = vec![ "crate::cmds::out::JVCustomOutput".to_string(), "crate::cmds::out::JVCustomOutput2".to_string(), "other::cmds::output::JVCustomOutputOutside".to_string(), ]; assert_eq!(result, expected); } }