From 92670ec92b555383fc31cf42b15d4ea38f8e9c8f Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Tue, 17 Mar 2026 14:47:25 +0800 Subject: Extract build-time generation code into separate crate --- Cargo.lock | 33 +++- Cargo.toml | 8 +- build.rs | 20 +-- gen.rs | 11 -- gen/Cargo.toml | 13 ++ gen/constants.rs | 36 ---- gen/env.rs | 136 -------------- gen/gen_commands_file.rs | 161 ----------------- gen/gen_compile_info.rs | 51 ------ gen/gen_completions_entries.rs | 79 --------- gen/gen_iscc_script.rs | 36 ---- gen/gen_mod_files.rs | 96 ---------- gen/gen_override_renderer.rs | 225 ----------------------- gen/gen_renderers_file.rs | 69 ------- gen/gen_specific_renderer.rs | 355 ------------------------------------- gen/resolve_types.rs | 114 ------------ gen/src/constants.rs | 36 ++++ gen/src/env.rs | 136 ++++++++++++++ gen/src/gen_commands_file.rs | 161 +++++++++++++++++ gen/src/gen_compile_info.rs | 51 ++++++ gen/src/gen_completions_entries.rs | 79 +++++++++ gen/src/gen_iscc_script.rs | 36 ++++ gen/src/gen_mod_files.rs | 96 ++++++++++ gen/src/gen_override_renderer.rs | 238 +++++++++++++++++++++++++ gen/src/gen_renderers_file.rs | 69 +++++++ gen/src/gen_specific_renderer.rs | 355 +++++++++++++++++++++++++++++++++++++ gen/src/lib.rs | 11 ++ gen/src/resolve_types.rs | 114 ++++++++++++ 28 files changed, 1435 insertions(+), 1390 deletions(-) delete mode 100644 gen.rs create mode 100644 gen/Cargo.toml delete mode 100644 gen/constants.rs delete mode 100644 gen/env.rs delete mode 100644 gen/gen_commands_file.rs delete mode 100644 gen/gen_compile_info.rs delete mode 100644 gen/gen_completions_entries.rs delete mode 100644 gen/gen_iscc_script.rs delete mode 100644 gen/gen_mod_files.rs delete mode 100644 gen/gen_override_renderer.rs delete mode 100644 gen/gen_renderers_file.rs delete mode 100644 gen/gen_specific_renderer.rs delete mode 100644 gen/resolve_types.rs create mode 100644 gen/src/constants.rs create mode 100644 gen/src/env.rs create mode 100644 gen/src/gen_commands_file.rs create mode 100644 gen/src/gen_compile_info.rs create mode 100644 gen/src/gen_completions_entries.rs create mode 100644 gen/src/gen_iscc_script.rs create mode 100644 gen/src/gen_mod_files.rs create mode 100644 gen/src/gen_override_renderer.rs create mode 100644 gen/src/gen_renderers_file.rs create mode 100644 gen/src/gen_specific_renderer.rs create mode 100644 gen/src/lib.rs create mode 100644 gen/src/resolve_types.rs diff --git a/Cargo.lock b/Cargo.lock index 0026df6..8c8e15a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1194,6 +1194,7 @@ dependencies = [ "framework", "hex_display", "jvlib", + "protocol", "serde", "sha1_hash", "sheet_system", @@ -1221,9 +1222,8 @@ dependencies = [ "just_enough_vcs", "just_fmt", "just_progress", - "just_template", + "jv_cli_gen", "log", - "regex", "render_system_macros", "ron", "rust-i18n", @@ -1254,13 +1254,25 @@ dependencies = [ [[package]] name = "just_template" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f77e78b7de1bd14c242241bbd802efd1e4b7a3717ea5f2eb05aba5e069d8b9" +checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225" dependencies = [ "just_fmt", ] +[[package]] +name = "jv_cli_gen" +version = "0.1.0-dev" +dependencies = [ + "chrono", + "just_fmt", + "just_template", + "regex", + "tokio", + "toml 0.9.8", +] + [[package]] name = "jvlib" version = "0.1.0" @@ -1586,6 +1598,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "protocol" +version = "0.1.0" +dependencies = [ + "framework", + "serde", + "sheet_system", + "thiserror", + "tokio", + "vault_system", + "workspace_system", +] + [[package]] name = "quote" version = "1.0.41" diff --git a/Cargo.toml b/Cargo.toml index 4eba850..9a957ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ homepage = "https://github.com/JustEnoughVCS/CommandLine/" [workspace] members = [ + "gen", "utils/", "tools/build_helper", "macros/cmd_system_macros", @@ -37,13 +38,8 @@ panic = "abort" strip = true [build-dependencies] -just_fmt = "0.1.2" - +jv_cli_gen = { path = "gen" } tokio = { version = "1", features = ["rt", "rt-multi-thread"] } -chrono = "0.4" -toml = "0.9" -regex = "1.12" -just_template = "0.1.0" [workspace.dependencies] proc-macro2 = "1.0" diff --git a/build.rs b/build.rs index d5c1444..39fcf05 100644 --- a/build.rs +++ b/build.rs @@ -1,18 +1,16 @@ use std::env; use std::path::PathBuf; -use crate::r#gen::{ - gen_commands_file::generate_commands_file, - gen_compile_info::generate_compile_info, - gen_completions_entries::generate_completions_file, - gen_iscc_script::generate_installer_script, - gen_mod_files::generate_collect_files, - gen_override_renderer::{generate_override_renderer, generate_override_renderers_list}, - gen_renderers_file::generate_renderers_file, - gen_specific_renderer::generate_specific_renderer, +use jv_cli_gen::gen_commands_file::generate_commands_file; +use jv_cli_gen::gen_compile_info::generate_compile_info; +use jv_cli_gen::gen_completions_entries::generate_completions_file; +use jv_cli_gen::gen_iscc_script::generate_installer_script; +use jv_cli_gen::gen_mod_files::generate_collect_files; +use jv_cli_gen::gen_override_renderer::{ + generate_override_renderer, generate_override_renderers_list, }; - -pub mod r#gen; +use jv_cli_gen::gen_renderers_file::generate_renderers_file; +use jv_cli_gen::gen_specific_renderer::generate_specific_renderer; #[tokio::main] async fn main() { diff --git a/gen.rs b/gen.rs deleted file mode 100644 index 3bd2719..0000000 --- a/gen.rs +++ /dev/null @@ -1,11 +0,0 @@ -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/Cargo.toml b/gen/Cargo.toml new file mode 100644 index 0000000..186082b --- /dev/null +++ b/gen/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jv_cli_gen" +edition = "2024" +version.workspace = true + +[dependencies] +just_fmt = "0.1.2" + +tokio = { version = "1", features = ["rt", "rt-multi-thread"] } +chrono = "0.4" +toml = "0.9" +regex = "1.12" +just_template = "0.1.3" diff --git a/gen/constants.rs b/gen/constants.rs deleted file mode 100644 index 140743d..0000000 --- a/gen/constants.rs +++ /dev/null @@ -1,36 +0,0 @@ -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/env.rs b/gen/env.rs deleted file mode 100644 index c45830e..0000000 --- a/gen/env.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::process::Command; - -pub fn get_author() -> Result> { - 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> { - 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> { - 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> { - 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/gen_commands_file.rs b/gen/gen_commands_file.rs deleted file mode 100644 index b68ac0a..0000000 --- a/gen/gen_commands_file.rs +++ /dev/null @@ -1,161 +0,0 @@ -use std::path::PathBuf; - -use just_fmt::pascal_case; -use just_template::{Template, tmpl, tmpl_param}; - -use crate::r#gen::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 = 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 = 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 = 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::>() - .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/gen_compile_info.rs b/gen/gen_compile_info.rs deleted file mode 100644 index 8d68d89..0000000 --- a/gen/gen_compile_info.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::path::PathBuf; - -use just_template::{Template, tmpl_param}; - -use crate::r#gen::{ - 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/gen_completions_entries.rs b/gen/gen_completions_entries.rs deleted file mode 100644 index 0e030e6..0000000 --- a/gen/gen_completions_entries.rs +++ /dev/null @@ -1,79 +0,0 @@ -use just_template::{Template, tmpl}; -use std::path::PathBuf; - -use crate::r#gen::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 = 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/gen_iscc_script.rs b/gen/gen_iscc_script.rs deleted file mode 100644 index 23328ab..0000000 --- a/gen/gen_iscc_script.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::path::PathBuf; - -use just_template::{Template, tmpl_param}; - -use crate::r#gen::{ - 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/gen_mod_files.rs b/gen/gen_mod_files.rs deleted file mode 100644 index 6e44eac..0000000 --- a/gen/gen_mod_files.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::path::PathBuf; - -use crate::r#gen::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/gen_override_renderer.rs b/gen/gen_override_renderer.rs deleted file mode 100644 index 2a8ba37..0000000 --- a/gen/gen_override_renderer.rs +++ /dev/null @@ -1,225 +0,0 @@ -use std::{collections::HashSet, path::PathBuf}; - -use just_template::{Template, tmpl}; -use regex::Regex; -use tokio::fs; - -use crate::r#gen::{ - 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*([^,]+)\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()); - } - - 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> { - cmd_output!(output, JVCustomOutput) - cmd_output!(output, JVCustomOutput2) - cmd_output!(output, JVCustomOutputNotExist) - cmd_output!(output, JVCustomOutputOutside) - } - "; - - 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/gen_renderers_file.rs b/gen/gen_renderers_file.rs deleted file mode 100644 index f16c504..0000000 --- a/gen/gen_renderers_file.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::path::PathBuf; - -use just_template::{Template, tmpl}; - -use crate::r#gen::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/gen_specific_renderer.rs b/gen/gen_specific_renderer.rs deleted file mode 100644 index 95e6900..0000000 --- a/gen/gen_specific_renderer.rs +++ /dev/null @@ -1,355 +0,0 @@ -use std::{collections::HashMap, path::PathBuf}; - -use just_template::{Template, tmpl}; -use regex::Regex; - -use crate::r#gen::{ - 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 - let mut renderer_matches: HashMap = 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) { - 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) { - 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::>().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::>() - .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 - "; - - 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 - "; - - 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 - "; - - 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 - "; - - 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 - "; - - 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 - "; - - 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 - "; - - 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 - "; - - 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)] - fn some_render(output: &SomeOutput) -> Result - "; - - 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 test10() { - const SITUATION: &str = " - #[result_renderer(MyRenderer<'a>)] - fn some_render(output: &SomeOutput<'a>) -> Result - "; - - 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 - "; - - 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 - "; - - 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 - "; - - 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 - "; - - 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 - "; - - 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/resolve_types.rs b/gen/resolve_types.rs deleted file mode 100644 index 6079abc..0000000 --- a/gen/resolve_types.rs +++ /dev/null @@ -1,114 +0,0 @@ -use regex::Regex; - -pub fn resolve_type_paths(code: &String, type_names: Vec) -> Option> { - 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, -) { - 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); - } - } - } -} 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> { + 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> { + 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> { + 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> { + 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 = 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 = 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 = 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::>() + .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 = 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 { + 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*([^,]+)\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 + let mut renderer_matches: HashMap = 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) { + 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) { + 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::>().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::>() + .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 + "; + + 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 + "; + + 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 + "; + + 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 + "; + + 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 + "; + + 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 + "; + + 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 + "; + + 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 + "; + + 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)] + fn some_render(output: &SomeOutput) -> Result + "; + + 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 test10() { + const SITUATION: &str = " + #[result_renderer(MyRenderer<'a>)] + fn some_render(output: &SomeOutput<'a>) -> Result + "; + + 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 + "; + + 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 + "; + + 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 + "; + + 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 + "; + + 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 + "; + + 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) -> Option> { + 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, +) { + 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); + } + } + } +} -- cgit