diff options
Diffstat (limited to 'gen/src')
| -rw-r--r-- | gen/src/constants.rs | 36 | ||||
| -rw-r--r-- | gen/src/env.rs | 136 | ||||
| -rw-r--r-- | gen/src/gen_commands_file.rs | 161 | ||||
| -rw-r--r-- | gen/src/gen_compile_info.rs | 51 | ||||
| -rw-r--r-- | gen/src/gen_completions_entries.rs | 79 | ||||
| -rw-r--r-- | gen/src/gen_iscc_script.rs | 36 | ||||
| -rw-r--r-- | gen/src/gen_mod_files.rs | 96 | ||||
| -rw-r--r-- | gen/src/gen_override_renderer.rs | 238 | ||||
| -rw-r--r-- | gen/src/gen_renderers_file.rs | 69 | ||||
| -rw-r--r-- | gen/src/gen_specific_renderer.rs | 355 | ||||
| -rw-r--r-- | gen/src/lib.rs | 11 | ||||
| -rw-r--r-- | gen/src/resolve_types.rs | 114 |
12 files changed, 1382 insertions, 0 deletions
diff --git a/gen/src/constants.rs b/gen/src/constants.rs new file mode 100644 index 0000000..140743d --- /dev/null +++ b/gen/src/constants.rs @@ -0,0 +1,36 @@ +pub const COMMANDS_PATH: &str = "./src/cmds/cmd/"; +pub const COMPLETIONS_PATH: &str = "./src/cmds/comp/"; +pub const RENDERERS_PATH: &str = "./src/cmds/renderer/"; + +pub const COMPILE_INFO_RS_TEMPLATE: &str = "./templates/compile_info.rs.template"; +pub const COMPILE_INFO_RS: &str = "./src/data/compile_info.rs"; + +pub const SETUP_JV_CLI_ISS_TEMPLATE: &str = "./templates/setup_jv_cli.iss.template"; +pub const SETUP_JV_CLI_ISS: &str = "./scripts/setup/windows/setup_jv_cli.iss"; + +pub const COMMAND_LIST_TEMPLATE: &str = "./templates/_commands.rs.template"; +pub const COMMAND_LIST: &str = "./src/systems/cmd/_commands.rs"; + +pub const COMPLETIONS_TEMPLATE: &str = "./templates/_comps.rs.template"; +pub const COMPLETIONS: &str = "./src/systems/comp/_comps.rs"; + +pub const OVERRIDE_RENDERER_DISPATCHER_TEMPLATE: &str = + "./templates/_override_renderer_dispatcher.rs.template"; +pub const OVERRIDE_RENDERER_DISPATCHER: &str = + "./src/systems/render/_override_renderer_dispatcher.rs"; + +pub const OVERRIDE_RENDERER_ENTRY_TEMPLATE: &str = + "./templates/_override_renderer_entry.rs.template"; +pub const OVERRIDE_RENDERER_ENTRY: &str = "./src/systems/render/_override_renderer_entry.rs"; + +pub const OVERRIDE_RENDERERS_TEMPLATE: &str = "./templates/_override_renderers.rs.template"; +pub const OVERRIDE_RENDERERS: &str = "./src/systems/render/_override_renderers.rs"; + +pub const SPECIFIC_RENDERER_MATCHING_TEMPLATE: &str = + "./templates/_specific_renderer_matching.rs.template"; +pub const SPECIFIC_RENDERER_MATCHING: &str = "./src/systems/render/_specific_renderer_matching.rs"; + +pub const REGISTRY_TOML: &str = "./.cargo/registry.toml"; + +pub const TEMPLATE_START: &str = "// -- TEMPLATE START --"; +pub const TEMPLATE_END: &str = "// -- TEMPLATE END --"; diff --git a/gen/src/env.rs b/gen/src/env.rs new file mode 100644 index 0000000..c45830e --- /dev/null +++ b/gen/src/env.rs @@ -0,0 +1,136 @@ +use std::process::Command; + +pub fn get_author() -> Result<String, Box<dyn std::error::Error>> { + let cargo_toml_path = std::path::Path::new("Cargo.toml"); + let cargo_toml_content = std::fs::read_to_string(cargo_toml_path)?; + let cargo_toml: toml::Value = toml::from_str(&cargo_toml_content)?; + + if let Some(package) = cargo_toml.get("package") { + if let Some(authors) = package.get("authors") { + if let Some(authors_array) = authors.as_array() { + if let Some(first_author) = authors_array.get(0) { + if let Some(author_str) = first_author.as_str() { + return Ok(author_str.to_string()); + } + } + } + } + } + + Err("Author not found in Cargo.toml".into()) +} + +pub fn get_site() -> Result<String, Box<dyn std::error::Error>> { + let cargo_toml_path = std::path::Path::new("Cargo.toml"); + let cargo_toml_content = std::fs::read_to_string(cargo_toml_path)?; + let cargo_toml: toml::Value = toml::from_str(&cargo_toml_content)?; + + if let Some(package) = cargo_toml.get("package") { + if let Some(homepage) = package.get("homepage") { + if let Some(site_str) = homepage.as_str() { + return Ok(site_str.to_string()); + } + } + } + + Err("Homepage not found in Cargo.toml".into()) +} + +pub fn get_platform(target: &str) -> String { + if target.contains("windows") { + "Windows".to_string() + } else if target.contains("linux") { + "Linux".to_string() + } else if target.contains("darwin") || target.contains("macos") { + "macOS".to_string() + } else if target.contains("android") { + "Android".to_string() + } else if target.contains("ios") { + "iOS".to_string() + } else { + "Unknown".to_string() + } +} + +pub fn get_toolchain() -> String { + let rustc_version = std::process::Command::new("rustc") + .arg("--version") + .output() + .ok() + .and_then(|output| String::from_utf8(output.stdout).ok()) + .unwrap_or_else(|| "unknown".to_string()) + .trim() + .to_string(); + + let channel = if rustc_version.contains("nightly") { + "nightly" + } else if rustc_version.contains("beta") { + "beta" + } else { + "stable" + }; + + format!("{} ({})", rustc_version, channel) +} + +pub fn get_version() -> String { + let cargo_toml_path = std::path::Path::new("Cargo.toml"); + let cargo_toml_content = match std::fs::read_to_string(cargo_toml_path) { + Ok(content) => content, + Err(_) => return "unknown".to_string(), + }; + + let cargo_toml: toml::Value = match toml::from_str(&cargo_toml_content) { + Ok(value) => value, + Err(_) => return "unknown".to_string(), + }; + + if let Some(workspace) = cargo_toml.get("workspace") { + if let Some(package) = workspace.get("package") { + if let Some(version) = package.get("version") { + if let Some(version_str) = version.as_str() { + return version_str.to_string(); + } + } + } + } + + "unknown".to_string() +} + +pub fn get_git_branch() -> Result<String, Box<dyn std::error::Error>> { + let output = Command::new("git") + .args(["branch", "--show-current"]) + .output()?; + + if output.status.success() { + let branch = String::from_utf8(output.stdout)?.trim().to_string(); + + if branch.is_empty() { + // Try to get HEAD reference if no branch (detached HEAD) + let output = Command::new("git") + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .output()?; + + if output.status.success() { + let head_ref = String::from_utf8(output.stdout)?.trim().to_string(); + return Ok(head_ref); + } + } else { + return Ok(branch); + } + } + + Err("Failed to get git branch".into()) +} + +pub fn get_git_commit() -> Result<String, Box<dyn std::error::Error>> { + let output = Command::new("git").args(["rev-parse", "HEAD"]).output()?; + + if output.status.success() { + let commit = String::from_utf8(output.stdout)?.trim().to_string(); + return Ok(commit); + } + + Err("Failed to get git commit".into()) +} diff --git a/gen/src/gen_commands_file.rs b/gen/src/gen_commands_file.rs new file mode 100644 index 0000000..c90e356 --- /dev/null +++ b/gen/src/gen_commands_file.rs @@ -0,0 +1,161 @@ +use std::path::PathBuf; + +use just_fmt::pascal_case; +use just_template::{Template, tmpl, tmpl_param}; + +use crate::constants::{COMMAND_LIST, COMMAND_LIST_TEMPLATE, COMMANDS_PATH, REGISTRY_TOML}; + +/// Generate registry file from Registry.toml configuration using just_template +pub async fn generate_commands_file(repo_root: &PathBuf) { + let template_path = repo_root.join(COMMAND_LIST_TEMPLATE); + let output_path = repo_root.join(COMMAND_LIST); + 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 command configurations + let mut all_commands: Vec<(String, String, String)> = Vec::new(); + let mut all_nodes: Vec<String> = Vec::new(); + + // Collect commands from registry.toml and COMMANDS_PATH in parallel + let (registry_collected, auto_collected) = tokio::join!( + async { + let mut commands: Vec<(String, String, String)> = Vec::new(); + let mut nodes: Vec<String> = Vec::new(); + + let Some(table) = config.as_table() else { + return (commands, nodes); + }; + + let Some(cmd_table_value) = table.get("cmd") else { + return (commands, nodes); + }; + + let Some(cmd_table) = cmd_table_value.as_table() else { + return (commands, nodes); + }; + + for (key, cmd_value) in cmd_table { + let Some(cmd_config) = cmd_value.as_table() else { + continue; + }; + + let Some(node_value) = cmd_config.get("node") else { + continue; + }; + + let Some(node_str) = node_value.as_str() else { + continue; + }; + + let Some(cmd_type_value) = cmd_config.get("type") else { + continue; + }; + + let Some(cmd_type_str) = cmd_type_value.as_str() else { + continue; + }; + + let n = node_str.replace(".", " "); + nodes.push(n.clone()); + commands.push((key.to_string(), n, cmd_type_str.to_string())); + } + + (commands, nodes) + }, + async { + let mut commands: Vec<(String, String, String)> = Vec::new(); + let mut nodes: Vec<String> = Vec::new(); + let commands_dir = repo_root.join(COMMANDS_PATH); + if commands_dir.exists() && commands_dir.is_dir() { + let mut entries = tokio::fs::read_dir(&commands_dir).await.unwrap(); + while let Some(entry) = entries.next_entry().await.unwrap() { + let path = entry.path(); + + if !path.is_file() { + continue; + } + + let extension = match path.extension() { + Some(ext) => ext, + None => continue, + }; + + if extension != "rs" { + continue; + } + + let file_name = match path.file_stem().and_then(|s| s.to_str()) { + Some(name) => name, + None => continue, + }; + + // Skip files that start with underscore + if file_name.starts_with('_') { + continue; + } + + // Convert filename to PascalCase + let pascal_name = pascal_case!(file_name); + + let key = file_name.to_string(); + let node = file_name.replace(".", " ").replace("_", " "); + let cmd_type = format!("cmds::cmd::{}::JV{}Command", file_name, pascal_name); + + nodes.push(node.clone()); + commands.push((key, node, cmd_type)); + } + } + (commands, nodes) + } + ); + + // Combine the results + let (mut registry_commands, mut registry_nodes) = registry_collected; + let (mut auto_commands, mut auto_nodes) = auto_collected; + + all_commands.append(&mut registry_commands); + all_commands.append(&mut auto_commands); + + all_nodes.append(&mut registry_nodes); + all_nodes.append(&mut auto_nodes); + + // Create template + let mut template = Template::from(template_content); + + for (key, node, cmd_type) in &all_commands { + tmpl!(template += { + command_match_arms { + (key = key, node_name = node, cmd_type = cmd_type) + } + }); + } + + let nodes_str = format!( + "[\n {}\n ]", + all_nodes + .iter() + .map(|node| format!("\"{}\".to_string()", node)) + .collect::<Vec<_>>() + .join(", ") + ); + + // Use insert_param for the NODES parameter + tmpl_param!(template, nodes = nodes_str); + + // Expand the template + let final_content = template.expand().unwrap(); + + // Write the generated code + tokio::fs::write(output_path, final_content).await.unwrap(); + + println!( + "Generated registry file with {} commands using just_template", + all_commands.len() + ); +} diff --git a/gen/src/gen_compile_info.rs b/gen/src/gen_compile_info.rs new file mode 100644 index 0000000..02666ac --- /dev/null +++ b/gen/src/gen_compile_info.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; + +use just_template::{Template, tmpl_param}; + +use crate::{ + constants::{COMPILE_INFO_RS, COMPILE_INFO_RS_TEMPLATE}, + env::{get_git_branch, get_git_commit, get_platform, get_toolchain, get_version}, +}; + +/// Generate compile info using just_template +pub async fn generate_compile_info(repo_root: &PathBuf) { + // Read the template code + let template_code = tokio::fs::read_to_string(repo_root.join(COMPILE_INFO_RS_TEMPLATE)) + .await + .unwrap(); + + // Get all the values needed for the template + let date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".to_string()); + let platform = get_platform(&target); + let toolchain = get_toolchain(); + let version = get_version(); + let branch = get_git_branch().unwrap_or_else(|_| "unknown".to_string()); + let commit = get_git_commit().unwrap_or_else(|_| "unknown".to_string()); + + // Create a Template instance + let mut template = Template::from(template_code); + + // Set all parameters + tmpl_param!( + template, + date = date, + target = target, + platform = platform, + toolchain = toolchain, + version = version, + branch = branch, + commit = commit + ); + + // Expand the template + let generated_code = template.expand().unwrap(); + + // Write the generated code + let compile_info_path = repo_root.join(COMPILE_INFO_RS); + tokio::fs::write(compile_info_path, generated_code) + .await + .unwrap(); + + println!("Generated compile_info.rs using just_template"); +} diff --git a/gen/src/gen_completions_entries.rs b/gen/src/gen_completions_entries.rs new file mode 100644 index 0000000..7f780b7 --- /dev/null +++ b/gen/src/gen_completions_entries.rs @@ -0,0 +1,79 @@ +use just_template::{Template, tmpl}; +use std::path::PathBuf; + +use crate::constants::{COMPLETIONS, COMPLETIONS_PATH, COMPLETIONS_TEMPLATE}; + +/// Generate completions file from comp directory using just_template +pub async fn generate_completions_file(repo_root: &PathBuf) { + let template_path = repo_root.join(COMPLETIONS_TEMPLATE); + let output_path = repo_root.join(COMPLETIONS); + let comps_dir = repo_root.join(COMPLETIONS_PATH); + + // Read the template + let template_content = tokio::fs::read_to_string(&template_path).await.unwrap(); + + // Collect all completion files + let mut all_completions: Vec<(String, String)> = Vec::new(); + let mut all_nodes: Vec<String> = Vec::new(); + + if comps_dir.exists() && comps_dir.is_dir() { + let mut entries = tokio::fs::read_dir(&comps_dir).await.unwrap(); + while let Some(entry) = entries.next_entry().await.unwrap() { + let path = entry.path(); + + if !path.is_file() { + continue; + } + + let extension = match path.extension() { + Some(ext) => ext, + None => continue, + }; + + if extension != "rs" { + continue; + } + + let file_name = match path.file_stem().and_then(|s| s.to_str()) { + Some(name) => name, + None => continue, + }; + + let node_name = just_fmt::lower_case!(file_name); + + all_completions.push((file_name.to_string(), node_name.clone())); + all_nodes.push(node_name); + } + } + + // Create template + let mut template = Template::from(template_content); + + // Generate match arms for each completion + for (comp_name, node_name) in &all_completions { + tmpl!(template += { + comp_match_arms { + (comp_name = comp_name, comp_node_name = node_name) + } + }); + } + + for node in all_nodes { + tmpl!(template += { + comp_node_name { + (comp_node_name = just_fmt::lower_case!(node)) + } + }); + } + + // Expand the template + let final_content = template.expand().unwrap(); + + // Write the generated code + tokio::fs::write(output_path, final_content).await.unwrap(); + + println!( + "Generated completions file with {} completions using just_template", + all_completions.len() + ); +} diff --git a/gen/src/gen_iscc_script.rs b/gen/src/gen_iscc_script.rs new file mode 100644 index 0000000..eabaab3 --- /dev/null +++ b/gen/src/gen_iscc_script.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; + +use just_template::{Template, tmpl_param}; + +use crate::{ + constants::{SETUP_JV_CLI_ISS, SETUP_JV_CLI_ISS_TEMPLATE}, + env::{get_author, get_site, get_version}, +}; + +/// Generate Inno Setup installer script (Windows only) using just_template +pub async fn generate_installer_script(repo_root: &PathBuf) { + let template_path = repo_root.join(SETUP_JV_CLI_ISS_TEMPLATE); + let output_path = repo_root.join(SETUP_JV_CLI_ISS); + + // Read the template + let template_content = tokio::fs::read_to_string(&template_path).await.unwrap(); + + // Get values for the template + let author = get_author().unwrap(); + let version = get_version(); + let site = get_site().unwrap(); + + // Create template + let mut template = Template::from(template_content); + + // Set all parameters + tmpl_param!(template, version = version, author = author, site = site); + + // Expand the template + let generated = template.expand().unwrap(); + + // Write the generated script + tokio::fs::write(output_path, generated).await.unwrap(); + + println!("Generated Inno Setup script using just_template"); +} diff --git a/gen/src/gen_mod_files.rs b/gen/src/gen_mod_files.rs new file mode 100644 index 0000000..d0b0800 --- /dev/null +++ b/gen/src/gen_mod_files.rs @@ -0,0 +1,96 @@ +use std::path::PathBuf; + +use crate::constants::REGISTRY_TOML; + +/// Generate collect files from directory structure +pub async fn generate_collect_files(repo_root: &PathBuf) { + // Read and parse the TOML configuration + let config_path = repo_root.join(REGISTRY_TOML); + let config_content = tokio::fs::read_to_string(&config_path).await.unwrap(); + let config: toml::Value = toml::from_str(&config_content).unwrap(); + + // Process each collect configuration + let collect_table = config.get("collect").and_then(|v| v.as_table()); + + let collect_table = match collect_table { + Some(table) => table, + None => return, + }; + + for (_collect_name, collect_config) in collect_table { + let config_table = match collect_config.as_table() { + Some(table) => table, + None => continue, + }; + + let path_str = match config_table.get("path").and_then(|v| v.as_str()) { + Some(path) => path, + None => continue, + }; + + let output_path = repo_root.join(path_str); + + // Extract directory name from the path (e.g., "src/renderers.rs" -> "renderers") + let dir_name = match output_path.file_stem().and_then(|s| s.to_str()) { + Some(name) => name.to_string(), + None => continue, + }; + + // Get the directory path for this collect type + // e.g., for "src/renderers.rs", we want "src/renderers/" + let output_parent = output_path.parent().unwrap_or_else(|| repo_root.as_path()); + let dir_path = output_parent.join(&dir_name); + + // Collect all .rs files in the directory (excluding the output file itself) + let mut modules = Vec::new(); + + if dir_path.exists() && dir_path.is_dir() { + for entry in std::fs::read_dir(&dir_path).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + + if !path.is_file() { + continue; + } + + let extension = match path.extension() { + Some(ext) => ext, + None => continue, + }; + + if extension != "rs" { + continue; + } + + let file_name = match path.file_stem().and_then(|s| s.to_str()) { + Some(name) => name, + None => continue, + }; + + // Skip files that start with underscore + if !file_name.starts_with('_') { + modules.push(file_name.to_string()); + } + } + } + + // Sort modules alphabetically + modules.sort(); + + // Generate the content + let mut content = String::new(); + for module in &modules { + content.push_str(&format!("pub mod {};\n", module)); + } + + // Write the file + tokio::fs::write(&output_path, content).await.unwrap(); + + println!( + "Generated {} with {} modules: {:?}", + path_str, + modules.len(), + modules + ); + } +} diff --git a/gen/src/gen_override_renderer.rs b/gen/src/gen_override_renderer.rs new file mode 100644 index 0000000..63b3464 --- /dev/null +++ b/gen/src/gen_override_renderer.rs @@ -0,0 +1,238 @@ +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<String> { + 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<Vec<String>> { + let mut output_types = Vec::new(); + + // Find all cmd_output! macros + let cmd_output_re = Regex::new(r"cmd_output!\s*\(\s*([^,]+)\s*=>\s*[^)]+\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*([^,]+)\s*=>\s*[^)]+\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()); + } + + 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); + } +} diff --git a/gen/src/gen_renderers_file.rs b/gen/src/gen_renderers_file.rs new file mode 100644 index 0000000..7ac1853 --- /dev/null +++ b/gen/src/gen_renderers_file.rs @@ -0,0 +1,69 @@ +use std::path::PathBuf; + +use just_template::{Template, tmpl}; + +use crate::constants::{ + OVERRIDE_RENDERER_DISPATCHER, OVERRIDE_RENDERER_DISPATCHER_TEMPLATE, REGISTRY_TOML, +}; + +/// Generate renderer list file from Registry.toml configuration using just_template +pub async fn generate_renderers_file(repo_root: &PathBuf) { + let template_path = repo_root.join(OVERRIDE_RENDERER_DISPATCHER_TEMPLATE); + let output_path = repo_root.join(OVERRIDE_RENDERER_DISPATCHER); + 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 configurations + let mut renderers = 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; + }; + let Some(renderer_type) = renderer_config.get("type").and_then(|v| v.as_str()) else { + continue; + }; + + renderers.push((name.to_string(), renderer_type.to_string())); + } + + // Create template + let mut template = Template::from(template_content); + + for (name, renderer_type) in &renderers { + tmpl!(template += { + renderer_match_arms { + (name = name, renderer_type = renderer_type) + } + }); + } + + let final_content = template.expand().unwrap(); + + // Write the generated code + tokio::fs::write(output_path, final_content).await.unwrap(); + + println!( + "Generated renderer list file with {} renderers using just_template", + renderers.len() + ); +} diff --git a/gen/src/gen_specific_renderer.rs b/gen/src/gen_specific_renderer.rs new file mode 100644 index 0000000..c68dcec --- /dev/null +++ b/gen/src/gen_specific_renderer.rs @@ -0,0 +1,355 @@ +use std::{collections::HashMap, path::PathBuf}; + +use just_template::{Template, tmpl}; +use regex::Regex; + +use crate::{ + constants::{RENDERERS_PATH, SPECIFIC_RENDERER_MATCHING, SPECIFIC_RENDERER_MATCHING_TEMPLATE}, + resolve_types::resolve_type_paths, +}; + +const RENDERER_TYPE_PREFIX: &str = "crate::"; + +/// Generate specific renderer matching file using just_template +pub async fn generate_specific_renderer(repo_root: &PathBuf) { + // Matches: HashMap<RendererTypeFullName, OutputTypeFullName> + let mut renderer_matches: HashMap<String, String> = HashMap::new(); + + let renderer_path = repo_root.join(RENDERERS_PATH); + collect_renderers(&renderer_path, &mut renderer_matches); + + let template_path = repo_root.join(SPECIFIC_RENDERER_MATCHING_TEMPLATE); + let output_path = repo_root.join(SPECIFIC_RENDERER_MATCHING); + + // 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 (renderer, output) in &renderer_matches { + let output_name = output.split("::").last().unwrap_or(output); + tmpl!(template += { + renderer_match_arms { + (output_type_name = output_name, output_type = output, renderer_type = renderer) + } + }); + } + + let final_content = template.expand().unwrap(); + + // Write the generated code + tokio::fs::write(output_path, final_content).await.unwrap(); + + println!( + "Generated specific renderer matching with {} renderers using just_template", + renderer_matches.len() + ); +} + +fn collect_renderers(dir_path: &PathBuf, matches: &mut HashMap<String, String>) { + if let Ok(entries) = std::fs::read_dir(dir_path) { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_dir() { + collect_renderers(&path, matches); + } else if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { + process_rs_file(&path, matches); + } + } + } + } +} + +fn process_rs_file(file_path: &PathBuf, matches: &mut HashMap<String, String>) { + let content = match std::fs::read_to_string(file_path) { + Ok(content) => content, + Err(_) => return, + }; + + let renderer_info = match get_renderer_types(&content) { + Some(info) => info, + None => return, + }; + + let (renderer_type, output_type) = renderer_info; + + let full_renderer_type = build_full_renderer_type(file_path, &renderer_type); + let full_output_type = resolve_type_paths(&content, vec![output_type]) + .unwrap() + .get(0) + .unwrap() + .clone(); + + matches.insert(full_renderer_type, full_output_type); +} + +fn build_full_renderer_type(file_path: &PathBuf, renderer_type: &str) -> String { + let relative_path = file_path + .strip_prefix(std::env::current_dir().unwrap()) + .unwrap_or(file_path); + let relative_path = relative_path.with_extension(""); + let path_str = relative_path.to_string_lossy(); + + // Normalize path separators and remove "./" prefix if present + let normalized_path = path_str + .replace('\\', "/") + .trim_start_matches("./") + .to_string(); + + let mut module_path = normalized_path.split('/').collect::<Vec<&str>>().join("::"); + + if module_path.starts_with("src") { + module_path = module_path.trim_start_matches("src").to_string(); + if module_path.starts_with("::") { + module_path = module_path.trim_start_matches("::").to_string(); + } + } + + format!("{}{}::{}", RENDERER_TYPE_PREFIX, module_path, renderer_type) +} + +pub fn get_renderer_types(code: &String) -> Option<(String, String)> { + let renderer_re = Regex::new(r"#\[result_renderer\(([^)]+)\)\]").unwrap(); + + let func_re = + Regex::new(r"(?:pub\s+)?(?:async\s+)?fn\s+\w+\s*\(\s*(?:mut\s+)?\w+\s*:\s*&([^),]+)\s*") + .unwrap(); + + let code_without_comments = code + .lines() + .filter(|line| !line.trim_start().starts_with("//")) + .collect::<Vec<&str>>() + .join("\n"); + + let renderer_captures = renderer_re.captures(&code_without_comments); + let func_captures = func_re.captures(&code_without_comments); + + match (renderer_captures, func_captures) { + (Some(renderer_cap), Some(func_cap)) => { + let renderer_type = renderer_cap[1].trim().to_string(); + let output_type = func_cap[1].trim().to_string(); + Some((renderer_type, output_type)) + } + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test1() { + const SITUATION: &str = " + #[result_renderer(MyRenderer)] + pub async fn render(data: &SomeOutput) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MyRenderer"); + assert_eq!(output, "SomeOutput"); + } + + #[test] + fn test2() { + const SITUATION: &str = " + #[result_renderer(MyRenderer)] + pub async fn some_render(output: &SomeOutput) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MyRenderer"); + assert_eq!(output, "SomeOutput"); + } + + #[test] + fn test3() { + const SITUATION: &str = " + #[result_renderer(MyRenderer)] + async fn some_render(output: &SomeOutput) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MyRenderer"); + assert_eq!(output, "SomeOutput"); + } + + #[test] + fn test4() { + const SITUATION: &str = " + #[result_renderer(MyRenderer)] + async pub fn some_render(output: &SomeOutput) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MyRenderer"); + assert_eq!(output, "SomeOutput"); + } + + #[test] + fn test5() { + const SITUATION: &str = " + #[result_renderer(MyRenderer)] + fn some_render(output: &SomeOutput2) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MyRenderer"); + assert_eq!(output, "SomeOutput2"); + } + + #[test] + fn test6() { + const SITUATION: &str = " + #[result__renderer(MyRenderer)] + fn some_render(output: &SomeOutput2) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!( + result.is_none(), + "Should fail to parse when annotation doesn't match" + ); + } + + #[test] + fn test7() { + const SITUATION: &str = " + #[result_renderer(MyRenderer)] + fn some_render() -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!( + result.is_none(), + "Should fail to parse when no function parameter" + ); + } + + #[test] + fn test8() { + const SITUATION: &str = " + #[result_renderer(MyRenderer)] + fn some_render(output: &SomeOutput, context: &Context) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MyRenderer"); + assert_eq!(output, "SomeOutput"); + } + + #[test] + fn test9() { + const SITUATION: &str = " + #[result_renderer(MyRenderer<T>)] + fn some_render(output: &SomeOutput<T>) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MyRenderer<T>"); + assert_eq!(output, "SomeOutput<T>"); + } + + #[test] + fn test10() { + const SITUATION: &str = " + #[result_renderer(MyRenderer<'a>)] + fn some_render(output: &SomeOutput<'a>) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MyRenderer<'a>"); + assert_eq!(output, "SomeOutput<'a>"); + } + + #[test] + fn test11() { + const SITUATION: &str = " + #[result_renderer( MyRenderer )] + fn some_render( output : & SomeOutput ) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MyRenderer"); + assert_eq!(output, "SomeOutput"); + } + + #[test] + fn test12() { + const SITUATION: &str = " + #[result_renderer(AnotherRenderer)] + fn some_render(output: &DifferentOutput) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "AnotherRenderer"); + assert_eq!(output, "DifferentOutput"); + } + + #[test] + fn test13() { + const SITUATION: &str = " + // #[result_renderer(WrongRenderer)] + #[result_renderer(CorrectRenderer)] + fn some_render(output: &CorrectOutput) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "CorrectRenderer"); + assert_eq!(output, "CorrectOutput"); + } + + #[test] + fn test14() { + const SITUATION: &str = " + #[result_renderer(MultiLineRenderer)] + fn some_render( + output: &MultiLineOutput + ) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MultiLineRenderer"); + assert_eq!(output, "MultiLineOutput"); + } + + #[test] + fn test15() { + const SITUATION: &str = " + #[result_renderer(MutRenderer)] + fn some_render(mut output: &MutOutput) -> Result<JVRenderResult, CmdRenderError> + "; + + let result = get_renderer_types(&SITUATION.to_string()); + assert!(result.is_some(), "Parse failed"); + let (renderer, output) = result.unwrap(); + assert_eq!(renderer, "MutRenderer"); + assert_eq!(output, "MutOutput"); + } +} diff --git a/gen/src/lib.rs b/gen/src/lib.rs new file mode 100644 index 0000000..3bd2719 --- /dev/null +++ b/gen/src/lib.rs @@ -0,0 +1,11 @@ +pub mod constants; +pub mod env; +pub mod gen_commands_file; +pub mod gen_compile_info; +pub mod gen_completions_entries; +pub mod gen_iscc_script; +pub mod gen_mod_files; +pub mod gen_override_renderer; +pub mod gen_renderers_file; +pub mod gen_specific_renderer; +pub mod resolve_types; diff --git a/gen/src/resolve_types.rs b/gen/src/resolve_types.rs new file mode 100644 index 0000000..6079abc --- /dev/null +++ b/gen/src/resolve_types.rs @@ -0,0 +1,114 @@ +use regex::Regex; + +pub fn resolve_type_paths(code: &String, type_names: Vec<String>) -> Option<Vec<String>> { + let mut type_mappings = std::collections::HashMap::new(); + + // Extract all use statements + let use_re = Regex::new(r"use\s+([^;]*(?:\{[^}]*\}[^;]*)*);").ok()?; + let mut use_statements = Vec::new(); + for cap in use_re.captures_iter(&code) { + use_statements.push(cap[1].to_string()); + } + + // Process each use statement to build type mappings + for stmt in &use_statements { + let stmt = stmt.trim(); + + if stmt.contains("::{") { + if let Some(pos) = stmt.find("::{") { + let base_path = &stmt[..pos]; + let content = &stmt[pos + 3..stmt.len() - 1]; + process_nested_use(base_path, content, &mut type_mappings); + } + } else { + // Process non-nested use statements + if let Some(pos) = stmt.rfind("::") { + let type_name = &stmt[pos + 2..]; + type_mappings.insert(type_name.to_string(), stmt.to_string()); + } else { + type_mappings.insert(stmt.to_string(), stmt.to_string()); + } + } + } + + // Resolve type names to full paths + let mut result = Vec::new(); + for type_name in type_names { + if let Some(full_path) = type_mappings.get(&type_name) { + result.push(full_path.clone()); + } + } + + Some(result) +} + +fn process_nested_use( + base_path: &str, + content: &str, + mappings: &mut std::collections::HashMap<String, String>, +) { + let mut items = Vec::new(); + let mut current_item = String::new(); + let mut brace_depth = 0; + + // Split nested content + for c in content.chars() { + match c { + '{' => { + brace_depth += 1; + current_item.push(c); + } + '}' => { + brace_depth -= 1; + current_item.push(c); + } + ',' => { + if brace_depth == 0 { + items.push(current_item.trim().to_string()); + current_item.clear(); + } else { + current_item.push(c); + } + } + _ => { + current_item.push(c); + } + } + } + + if !current_item.trim().is_empty() { + items.push(current_item.trim().to_string()); + } + + // Process each item + for item in items { + if item.is_empty() { + continue; + } + + if item.contains("::{") { + if let Some(pos) = item.find("::{") { + let sub_path = &item[..pos]; + let sub_content = &item[pos + 3..item.len() - 1]; + let new_base = if base_path.is_empty() { + sub_path.to_string() + } else { + format!("{}::{}", base_path, sub_path) + }; + process_nested_use(&new_base, sub_content, mappings); + } + } else { + let full_path = if base_path.is_empty() { + item.to_string() + } else { + format!("{}::{}", base_path, item) + }; + if let Some(pos) = item.rfind("::") { + let type_name = &item[pos + 2..]; + mappings.insert(type_name.to_string(), full_path); + } else { + mappings.insert(item.to_string(), full_path); + } + } + } +} |
