From d19e5d84ee21502fd3440511d4ffb1ee1f49d3b2 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 4 Feb 2026 00:27:16 +0800 Subject: Refactor build system and implement complete renderer system - Split monolithic build.rs into modular async generators - Add renderer override system with type-safe dispatch - Implement command template macro for consistent command definitions - Add proc-macro crates for command and renderer systems - Reorganize directory structure for better separation of concerns - Update documentation to reflect new architecture --- .cargo/registry.toml | 12 +- .gitignore | 1 + CONTRIBUTING.md | 16 +- CONTRIBUTING_zh_CN.md | 16 +- Cargo.lock | 39 ++ Cargo.toml | 17 +- build.rs | 593 ++------------------- gen.rs | 10 + gen/constants.rs | 29 + gen/env.rs | 136 +++++ gen/gen_commands_file.rs | 188 +++++++ gen/gen_compile_info.rs | 37 ++ gen/gen_iscc_script.rs | 25 + gen/gen_mod_files.rs | 96 ++++ gen/gen_override_renderer.rs | 188 +++++++ gen/gen_renderers_file.rs | 97 ++++ gen/gen_specific_renderer.rs | 383 +++++++++++++ gen/resolve_types.rs | 114 ++++ macros/cmd_system_macros/Cargo.toml | 12 + macros/cmd_system_macros/src/lib.rs | 92 ++++ macros/render_system_macros/Cargo.toml | 12 + macros/render_system_macros/src/lib.rs | 143 +++++ resources/locales/jvn/en.yml | 22 + resources/locales/jvn/zh-CN.yml | 22 + rust-analyzer.toml | 51 ++ src/bin/jvn.rs | 17 +- src/cmds.rs | 1 + src/cmds/cmd/status.rs | 253 +++++---- src/cmds/override.rs | 1 + src/cmds/override/renderer/json.rs | 17 + src/cmds/override/renderer/json_pretty.rs | 16 + src/cmds/renderer/json.rs | 27 - src/cmds/renderer/json_pretty.rs | 26 - src/cmds/renderer/status.rs | 32 +- src/systems.rs | 1 + src/systems/cmd.rs | 4 +- src/systems/cmd/cmd_system.rs | 106 ++-- src/systems/cmd/errors.rs | 12 + src/systems/cmd/macros.rs | 166 ++++++ src/systems/cmd/processer.rs | 4 +- src/systems/cmd/renderer.rs | 54 -- src/systems/render.rs | 2 + src/systems/render/render_system.rs | 14 + src/systems/render/renderer.rs | 53 ++ templates/_commands.rs.template | 40 ++ .../_override_renderer_dispatcher.rs.template | 13 + templates/_override_renderer_entry.rs.template | 13 + templates/_registry.rs.template | 33 -- templates/_renderers.rs.template | 16 - templates/_specific_renderer_matching.rs.template | 14 + 50 files changed, 2360 insertions(+), 926 deletions(-) create mode 100644 gen.rs create mode 100644 gen/constants.rs create mode 100644 gen/env.rs create mode 100644 gen/gen_commands_file.rs create mode 100644 gen/gen_compile_info.rs create mode 100644 gen/gen_iscc_script.rs create mode 100644 gen/gen_mod_files.rs create mode 100644 gen/gen_override_renderer.rs create mode 100644 gen/gen_renderers_file.rs create mode 100644 gen/gen_specific_renderer.rs create mode 100644 gen/resolve_types.rs create mode 100644 macros/cmd_system_macros/Cargo.toml create mode 100644 macros/cmd_system_macros/src/lib.rs create mode 100644 macros/render_system_macros/Cargo.toml create mode 100644 macros/render_system_macros/src/lib.rs create mode 100644 rust-analyzer.toml create mode 100644 src/cmds/override.rs create mode 100644 src/cmds/override/renderer/json.rs create mode 100644 src/cmds/override/renderer/json_pretty.rs delete mode 100644 src/cmds/renderer/json.rs delete mode 100644 src/cmds/renderer/json_pretty.rs create mode 100644 src/systems/cmd/macros.rs delete mode 100644 src/systems/cmd/renderer.rs create mode 100644 src/systems/render.rs create mode 100644 src/systems/render/render_system.rs create mode 100644 src/systems/render/renderer.rs create mode 100644 templates/_commands.rs.template create mode 100644 templates/_override_renderer_dispatcher.rs.template create mode 100644 templates/_override_renderer_entry.rs.template delete mode 100644 templates/_registry.rs.template delete mode 100644 templates/_renderers.rs.template create mode 100644 templates/_specific_renderer_matching.rs.template diff --git a/.cargo/registry.toml b/.cargo/registry.toml index c1a712d..9077c4b 100644 --- a/.cargo/registry.toml +++ b/.cargo/registry.toml @@ -16,19 +16,14 @@ # Only register renderers here that need to be overridden using the `--renderer` flag. # After registration, you can use the format `command --renderer renderer_name` to override the renderer. -# Default Renderer -[renderer.default] -name = "default" -type = "Renderer" - # Json Renderer [renderer.json] name = "json" -type = "crate::cmds::renderer::json::JVResultJsonRenderer" +type = "crate::cmds::r#override::renderer::json" [renderer.json_pretty] name = "json-pretty" -type = "crate::cmds::renderer::json_pretty::JVResultPrettyJsonRenderer" +type = "crate::cmds::r#override::renderer::json_pretty" #################### ### Auto-Collect ### @@ -51,3 +46,6 @@ path = "src/cmds/out.rs" [collect.renderers] path = "src/cmds/renderer.rs" + +[collect.override_renderers] +path = "src/cmds/override/renderer.rs" diff --git a/.gitignore b/.gitignore index 4e45244..45c7cf8 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ _*.rs /src/cmds/in.rs /src/cmds/out.rs /src/cmds/renderer.rs +/src/cmds/override/renderer.rs # Symbolic links and shortcuts created by scripts /deploy.lnk diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a054644..514db9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,18 +97,18 @@ A complete command consists of the following components, organized by module: | Module | Path | Description | |--------|------|-------------| -| **Command Definition** | `src/cmds/` | The main logic implementation of the command. | -| **Argument Definition** | `src/args/` | Defines command-line inputs using `clap`. | -| **Input Data** | `src/inputs/` | User input data during command execution. | -| **Collected Data** | `src/collects/` | Data collected locally during command execution. | -| **Output Data** | `src/outputs/` | The command's output data. | -| **Renderer** | `src/renderers/` | The default presentation method for data. | +| **Command Definition** | `src/cmds/cmd/` | The main logic implementation of the command. | +| **Argument Definition** | `src/cmds/arg/` | Defines command-line inputs using `clap`. | +| **Input Data** | `src/cmds/in/` | User input data during command execution. | +| **Collected Data** | `src/cmds/collect/` | Data collected locally during command execution. | +| **Output Data** | `src/cmds/out/` | The command's output data. | +| **Renderer** | `src/cmds/renderer/` | The default presentation method for data. | ### Naming Conventions -- **File Naming**: Follow the format of `src/cmds/status.rs`, i.e., use the command name as the filename. +- **File Naming**: Follow the format of `src/cmds/cmd/status.rs`, i.e., use the command name as the filename. - **Multi-level Subcommands**: In the `cmds` directory, use the `sub_subsub.rs` format for filenames (e.g., `sheet_drop.rs`). - **Struct Naming**: - Command Struct: `JV{Subcommand}{Subsubcommand}Command` (e.g., `JVSheetDropCommand`). @@ -122,7 +122,7 @@ A complete command consists of the following components, organized by module: ### Other Development Conventions -- **Utility Functions**: Reusable functionality should be placed in the `src/utils/` directory (e.g., `src/utils/feat.rs`). Test code should be written directly within the corresponding feature file. +- **Utility Functions**: Reusable functionality should be placed in the `utils/` directory (e.g., `utils/feat.rs`). Test code should be written directly within the corresponding feature file. - **Special Files**: `.rs` files starting with an underscore `_` are excluded by the `.gitignore` rule and will not be tracked by Git. - **File Movement**: If you need to move a file, be sure to use the `git mv` command or ensure the file is already tracked by Git. The commit message should explain the reason for the move. - **Frontend/Backend Responsibilities**: The frontend (command-line interface) should remain lightweight, primarily responsible for data collection and presentation. Any operation that needs to modify workspace data must call the interfaces provided by the core library. diff --git a/CONTRIBUTING_zh_CN.md b/CONTRIBUTING_zh_CN.md index 480cf6c..144f291 100644 --- a/CONTRIBUTING_zh_CN.md +++ b/CONTRIBUTING_zh_CN.md @@ -98,18 +98,18 @@ sources ~/.../JustEnoughVCS/CommandLine/.temp/deploy/jv_cli.sh | 模块 | 路径 | 说明 | |------|------|------| -| **命令定义** | `src/cmds/` | 命令的主逻辑实现。 | -| **参数定义** | `src/args/` | 使用 `clap` 定义命令行输入。 | -| **输入数据** | `src/inputs/` | 命令运行阶段的用户输入数据。 | -| **收集数据** | `src/collects/` | 命令运行阶段从本地收集的数据。 | -| **输出数据** | `src/outputs/` | 命令的输出数据。 | -| **渲染器** | `src/renderers/` | 数据的默认呈现方式。 | +| **命令定义** | `src/cmds/cmd/` | 命令的主逻辑实现。 | +| **参数定义** | `src/cmds/arg/` | 使用 `clap` 定义命令行输入。 | +| **输入数据** | `src/cmds/in/` | 命令运行阶段的用户输入数据。 | +| **收集数据** | `src/cmds/collect/` | 命令运行阶段从本地收集的数据。 | +| **输出数据** | `src/cmds/out/` | 命令的输出数据。 | +| **渲染器** | `src/cmds/renderer/` | 数据的默认呈现方式。 | ### 命名规范 -- **文件命名**: 请遵循 `src/cmds/status.rs` 的格式,即使用命令名称作为文件名 +- **文件命名**: 请遵循 `src/cmds/cmd/status.rs` 的格式,即使用命令名称作为文件名 - **多级子命令**: 在 `cmds` 目录下,使用 `sub_subsub.rs` 格式命名文件(例如:`sheet_drop.rs`) - **结构体命名**: - 命令结构体: `JV{Subcommand}{Subsubcommand}Command` (例如:`JVSheetDropCommand`) @@ -123,7 +123,7 @@ sources ~/.../JustEnoughVCS/CommandLine/.temp/deploy/jv_cli.sh ### 其他开发约定 -- **工具函数**: 可复用的功能应置于 `src/utils/` 目录下(例如 `src/utils/feat.rs`),测试代码应直接写在对应的功能文件内 +- **工具函数**: 可复用的功能应置于 `utils/` 目录下(例如 `utils/feat.rs`),测试代码应直接写在对应的功能文件内 - **特殊文件**: 以 `_` 下划线开头的 `.rs` 文件已被 `.gitignore` 规则排除,不会被 Git 追踪 - **文件移动**: 如需移动文件,请务必使用 `git mv` 命令或确保文件已被 Git 追踪。在提交信息中应说明移动原因 - **前后端职责**: 前端(命令行界面)应保持轻量,主要负责数据收集与展示。任何需要修改工作区数据的操作,都必须调用核心库提供的接口 diff --git a/Cargo.lock b/Cargo.lock index 0758f74..457f0d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,6 +409,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "cmd_system_macros" +version = "0.1.0-dev" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -681,6 +690,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "fiat-crypto" version = "0.3.0" @@ -1014,10 +1034,14 @@ dependencies = [ "chrono", "clap", "cli_utils", + "cmd_system_macros", "colored", "crossterm", + "erased-serde", "just_enough_vcs", "log", + "regex", + "render_system_macros", "rust-i18n", "serde", "serde_json", @@ -1451,6 +1475,15 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "render_system_macros" +version = "0.1.0-dev" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ring" version = "0.17.14" @@ -2053,6 +2086,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.19.0" diff --git a/Cargo.toml b/Cargo.toml index 28d79d6..340b41e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,12 @@ authors = ["JustEnoughVCS Team"] homepage = "https://github.com/JustEnoughVCS/CommandLine/" [workspace] -members = ["utils/", "tools/build_helper"] +members = [ + "utils/", + "tools/build_helper", + "macros/render_system_macros", + "macros/cmd_system_macros" +] [workspace.package] version = "0.1.0-dev" @@ -34,13 +39,21 @@ strip = true # Just Enough VCS String Formatter string_proc = { path = "../VersionControl/utils/string_proc/" } +tokio = { version = "1", features = ["rt", "rt-multi-thread"] } chrono = "0.4" toml = "0.9" +regex = "1.12" [dependencies] # Just Enough VCS just_enough_vcs = { path = "../VersionControl", features = ["all"] } +# RenderSystem Macros +render_system_macros = { path = "macros/render_system_macros" } + +# CommandSystem Macros +cmd_system_macros = { path = "macros/cmd_system_macros" } + # CommandLine Utilities cli_utils = { path = "utils" } @@ -48,6 +61,8 @@ cli_utils = { path = "utils" } thiserror = "2.0.17" # Serialize +# What the heck, why does this crate use kebab-case instead of snake_case ???? +erased_serde = { package = "erased-serde", version = "0.4" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/build.rs b/build.rs index 07b5c10..ad02511 100644 --- a/build.rs +++ b/build.rs @@ -1,563 +1,54 @@ use std::env; use std::path::PathBuf; -use std::process::Command; -use string_proc::pascal_case; +use crate::r#gen::{ + gen_commands_file::generate_commands_file, gen_compile_info::generate_compile_info, + gen_iscc_script::generate_installer_script, gen_mod_files::generate_collect_files, + gen_override_renderer::generate_override_renderer, gen_renderers_file::generate_renderers_file, + gen_specific_renderer::generate_specific_renderer, +}; -const COMMANDS_PATH: &str = "./src/cmds/cmd/"; +pub mod r#gen; -const COMPILE_INFO_RS_TEMPLATE: &str = "./templates/compile_info.rs.template"; -const COMPILE_INFO_RS: &str = "./src/data/compile_info.rs"; - -const SETUP_JV_CLI_ISS_TEMPLATE: &str = "./templates/setup_jv_cli.iss.template"; -const SETUP_JV_CLI_ISS: &str = "./scripts/setup/windows/setup_jv_cli.iss"; - -const REGISTRY_RS_TEMPLATE: &str = "./templates/_registry.rs.template"; -const REGISTRY_RS: &str = "./src/systems/cmd/_registry.rs"; - -const RENDERER_LIST_TEMPLATE: &str = "./templates/_renderers.rs.template"; -const RENDERER_LIST: &str = "./src/systems/cmd/_renderers.rs"; - -const REGISTRY_TOML: &str = "./.cargo/registry.toml"; - -fn main() { +#[tokio::main] +async fn main() { println!("cargo:rerun-if-env-changed=FORCE_BUILD"); - let repo_root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - - if cfg!(target_os = "windows") { - // Only generate installer script on Windows - if let Err(e) = generate_installer_script(&repo_root) { - eprintln!("Failed to generate installer script: {}", e); - std::process::exit(1); - } - } - - if let Err(e) = generate_compile_info(&repo_root) { - eprintln!("Failed to generate compile info: {}", e); - std::process::exit(1); - } - - if let Err(e) = generate_cmd_registry_file(&repo_root) { - eprintln!("Failed to generate registry file: {}", e); - std::process::exit(1); - } - - if let Err(e) = generate_renderer_list_file(&repo_root) { - eprintln!("Failed to generate renderer list: {}", e); - std::process::exit(1); - } - - if let Err(e) = generate_collect_files(&repo_root) { - eprintln!("Failed to generate collect files: {}", e); - std::process::exit(1); - } -} - -/// Generate Inno Setup installer script (Windows only) -fn generate_installer_script(repo_root: &PathBuf) -> Result<(), Box> { - let template_path = repo_root.join(SETUP_JV_CLI_ISS_TEMPLATE); - let output_path = repo_root.join(SETUP_JV_CLI_ISS); - - let template = std::fs::read_to_string(&template_path)?; - - let author = get_author()?; - let version = get_version(); - let site = get_site()?; - - let generated = template - .replace("<<>>", &author) - .replace("<<>>", &version) - .replace("<<>>", &site); - - std::fs::write(output_path, generated)?; - Ok(()) -} - -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()) -} - -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()) -} - -/// Generate compile info -fn generate_compile_info(repo_root: &PathBuf) -> Result<(), Box> { - // Read the template code - let template_code = std::fs::read_to_string(repo_root.join(COMPILE_INFO_RS_TEMPLATE))?; - - let date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); - let target = 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()); - - let generated_code = template_code - .replace("{date}", &date) - .replace("{target}", &target) - .replace("{platform}", &platform) - .replace("{toolchain}", &toolchain) - .replace("{version}", &version) - .replace("{branch}", &branch) - .replace("{commit}", &commit); - - // Write the generated code - let compile_info_path = repo_root.join(COMPILE_INFO_RS); - std::fs::write(compile_info_path, generated_code)?; - - Ok(()) -} - -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() - } -} - -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) -} - -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() -} - -/// Get current git branch -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()) -} - -/// Get current git commit hash -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()) -} - -/// Generate registry file from Registry.toml configuration -fn generate_cmd_registry_file(repo_root: &PathBuf) -> Result<(), Box> { - let template_path = repo_root.join(REGISTRY_RS_TEMPLATE); - let output_path = repo_root.join(REGISTRY_RS); - let config_path = repo_root.join(REGISTRY_TOML); - - // Read the template - let template = std::fs::read_to_string(&template_path)?; - - // Read and parse the TOML configuration - let config_content = std::fs::read_to_string(&config_path)?; - let config: toml::Value = toml::from_str(&config_content)?; - - // Collect all command configurations - let mut commands = Vec::new(); - let mut nodes = Vec::new(); - - // First, collect commands from registry.toml - if let Some(table) = config.as_table() { - if let Some(cmd_table) = table.get("cmd") { - if let Some(cmd_table) = cmd_table.as_table() { - for (key, cmd_value) in cmd_table { - if let Some(cmd_config) = cmd_value.as_table() { - if let (Some(node), Some(cmd_type)) = ( - cmd_config.get("node").and_then(|v| v.as_str()), - cmd_config.get("type").and_then(|v| v.as_str()), - ) { - let n = node.replace(".", " "); - nodes.push(n.clone()); - commands.push((key.to_string(), n, cmd_type.to_string())); - } - } + let repo_root = std::sync::Arc::new(PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())); + + let _ = tokio::join!( + tokio::spawn({ + let repo_root = repo_root.clone(); + async move { generate_compile_info(&repo_root).await } + }), + tokio::spawn({ + let repo_root = repo_root.clone(); + async move { generate_commands_file(&repo_root).await } + }), + tokio::spawn({ + let repo_root = repo_root.clone(); + async move { generate_renderers_file(&repo_root).await } + }), + tokio::spawn({ + let repo_root = repo_root.clone(); + async move { generate_collect_files(&repo_root).await } + }), + tokio::spawn({ + let repo_root = repo_root.clone(); + async move { generate_override_renderer(&repo_root).await } + }), + tokio::spawn({ + let repo_root = repo_root.clone(); + async move { generate_specific_renderer(&repo_root).await } + }), + tokio::spawn({ + async move { + if cfg!(target_os = "windows") { + // Only generate installer script on Windows + let repo_root = repo_root.clone(); + generate_installer_script(&repo_root).await } } - } - } - - // Then, automatically register commands from COMMANDS_PATH - let commands_dir = repo_root.join(COMMANDS_PATH); - if commands_dir.exists() && commands_dir.is_dir() { - for entry in std::fs::read_dir(&commands_dir)? { - let entry = entry?; - 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)); - } - } - - // Extract the node_if template from the template content - const PROCESS_MARKER: &str = "// PROCESS"; - const TEMPLATE_START: &str = "// -- TEMPLATE START --"; - const TEMPLATE_END: &str = "// -- TEMPLATE END --"; - const LINE: &str = "<>"; - const NODES: &str = "<>"; - - let template_start_index = template - .find(TEMPLATE_START) - .ok_or("Template start marker not found")?; - let template_end_index = template - .find(TEMPLATE_END) - .ok_or("Template end marker not found")?; - - let template_slice = &template[template_start_index..template_end_index + TEMPLATE_END.len()]; - let node_if_template = template_slice - .trim_start_matches(TEMPLATE_START) - .trim_end_matches(TEMPLATE_END) - .trim_matches('\n'); - - // Generate the match arms for each command - let match_arms: String = commands - .iter() - .map(|(key, node, cmd_type)| { - node_if_template - .replace("<>", key) - .replace("<>", node) - .replace("<>", cmd_type) - .trim_matches('\n') - .to_string() - }) - .collect::>() - .join("\n"); - - let nodes_str = format!( - "[\n {}\n ]", - nodes - .iter() - .map(|node| format!("\"{}\".to_string()", node)) - .collect::>() - .join(", ") - ); - - // Replace the template section with the generated match arms - let final_content = template - .replace(node_if_template, "") - .replace(TEMPLATE_START, "") - .replace(TEMPLATE_END, "") - .replace(PROCESS_MARKER, &match_arms) - .lines() - .filter(|line| !line.trim().is_empty()) - .collect::>() - .join("\n") - .replace(LINE, "") - .replace(NODES, nodes_str.as_str()); - - // Write the generated code - std::fs::write(output_path, final_content)?; - - println!("Generated registry file with {} commands", commands.len()); - Ok(()) -} - -/// Generate renderer list file from Registry.toml configuration -fn generate_renderer_list_file(repo_root: &PathBuf) -> Result<(), Box> { - let template_path = repo_root.join(RENDERER_LIST_TEMPLATE); - let output_path = repo_root.join(RENDERER_LIST); - let config_path = repo_root.join(REGISTRY_TOML); - - // Read the template - let template = std::fs::read_to_string(&template_path)?; - - // Read and parse the TOML configuration - let config_content = std::fs::read_to_string(&config_path)?; - let config: toml::Value = toml::from_str(&config_content)?; - - // Collect all renderer configurations - let mut renderers = Vec::new(); - - if let Some(table) = config.as_table() { - if let Some(renderer_table) = table.get("renderer") { - if let Some(renderer_table) = renderer_table.as_table() { - for (_, renderer_value) in renderer_table { - if let Some(renderer_config) = renderer_value.as_table() { - if let (Some(name), Some(renderer_type)) = ( - renderer_config.get("name").and_then(|v| v.as_str()), - renderer_config.get("type").and_then(|v| v.as_str()), - ) { - renderers.push((name.to_string(), renderer_type.to_string())); - } - } - } - } - } - } - - // Extract the template section from the template content - const MATCH_MARKER: &str = "// MATCH"; - const TEMPLATE_START: &str = "// -- TEMPLATE START --"; - const TEMPLATE_END: &str = "// -- TEMPLATE END --"; - - let template_start_index = template - .find(TEMPLATE_START) - .ok_or("Template start marker not found")?; - let template_end_index = template - .find(TEMPLATE_END) - .ok_or("Template end marker not found")?; - - let template_slice = &template[template_start_index..template_end_index + TEMPLATE_END.len()]; - let renderer_template = template_slice - .trim_start_matches(TEMPLATE_START) - .trim_end_matches(TEMPLATE_END) - .trim_matches('\n'); - - // Generate the match arms for each renderer - let match_arms: String = renderers - .iter() - .map(|(name, renderer_type)| { - renderer_template - .replace("<>", name) - .replace("RendererType", renderer_type) - .trim_matches('\n') - .to_string() }) - .collect::>() - .join("\n"); - - // Replace the template section with the generated match arms - let final_content = template - .replace(renderer_template, "") - .replace(TEMPLATE_START, "") - .replace(TEMPLATE_END, "") - .replace(MATCH_MARKER, &match_arms) - .lines() - .filter(|line| !line.trim().is_empty()) - .collect::>() - .join("\n"); - - // Write the generated code - std::fs::write(output_path, final_content)?; - - println!( - "Generated renderer list file with {} renderers", - renderers.len() ); - Ok(()) -} - -/// Generate collect files from directory structure -fn generate_collect_files(repo_root: &PathBuf) -> Result<(), Box> { - // Read and parse the TOML configuration - let config_path = repo_root.join(REGISTRY_TOML); - let config_content = std::fs::read_to_string(&config_path)?; - let config: toml::Value = toml::from_str(&config_content)?; - - // 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 Ok(()), - }; - - 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)? { - let entry = entry?; - 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 - std::fs::write(&output_path, content)?; - - println!( - "Generated {} with {} modules: {:?}", - path_str, - modules.len(), - modules - ); - } - - Ok(()) } diff --git a/gen.rs b/gen.rs new file mode 100644 index 0000000..da64173 --- /dev/null +++ b/gen.rs @@ -0,0 +1,10 @@ +pub mod constants; +pub mod env; +pub mod gen_commands_file; +pub mod gen_compile_info; +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/constants.rs b/gen/constants.rs new file mode 100644 index 0000000..e26317f --- /dev/null +++ b/gen/constants.rs @@ -0,0 +1,29 @@ +pub const COMMANDS_PATH: &str = "./src/cmds/cmd/"; +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 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 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 new file mode 100644 index 0000000..c45830e --- /dev/null +++ b/gen/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/gen_commands_file.rs b/gen/gen_commands_file.rs new file mode 100644 index 0000000..a6b7212 --- /dev/null +++ b/gen/gen_commands_file.rs @@ -0,0 +1,188 @@ +use std::path::PathBuf; + +use string_proc::pascal_case; + +use crate::r#gen::constants::{ + COMMAND_LIST, COMMAND_LIST_TEMPLATE, COMMANDS_PATH, REGISTRY_TOML, TEMPLATE_END, TEMPLATE_START, +}; + +/// Generate registry file from Registry.toml configuration +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 = 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 commands = Vec::new(); + let mut nodes = Vec::new(); + + // Collect commands from registry.toml and COMMANDS_PATH in parallel + let (registry_collected, auto_collected) = tokio::join!( + async { + let mut commands = Vec::new(); + let mut nodes = 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::new(); + let mut nodes = 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; + + commands.append(&mut registry_commands); + commands.append(&mut auto_commands); + nodes.append(&mut registry_nodes); + nodes.append(&mut auto_nodes); + + // Extract the node_if template from the template content + const PROCESS_MARKER: &str = "// PROCESS"; + const LINE: &str = "<>"; + const NODES: &str = "<>"; + + let template_start_index = template + .find(TEMPLATE_START) + .ok_or("Template start marker not found") + .unwrap(); + let template_end_index = template + .find(TEMPLATE_END) + .ok_or("Template end marker not found") + .unwrap(); + + let template_slice = &template[template_start_index..template_end_index + TEMPLATE_END.len()]; + let node_if_template = template_slice + .trim_start_matches(TEMPLATE_START) + .trim_end_matches(TEMPLATE_END) + .trim_matches('\n'); + + // Generate the match arms for each command + let match_arms: String = commands + .iter() + .map(|(key, node, cmd_type)| { + node_if_template + .replace("<>", key) + .replace("<>", node) + .replace("<>", cmd_type) + .trim_matches('\n') + .to_string() + }) + .collect::>() + .join("\n"); + + let nodes_str = format!( + "[\n {}\n ]", + nodes + .iter() + .map(|node| format!("\"{}\".to_string()", node)) + .collect::>() + .join(", ") + ); + + // Replace the template section with the generated match arms + let final_content = template + .replace(node_if_template, "") + .replace(TEMPLATE_START, "") + .replace(TEMPLATE_END, "") + .replace(PROCESS_MARKER, &match_arms) + .lines() + .filter(|line| !line.trim().is_empty()) + .collect::>() + .join("\n") + .replace(LINE, "") + .replace(NODES, nodes_str.as_str()); + + // Write the generated code + tokio::fs::write(output_path, final_content).await.unwrap(); + + println!("Generated registry file with {} commands", commands.len()); +} diff --git a/gen/gen_compile_info.rs b/gen/gen_compile_info.rs new file mode 100644 index 0000000..5af030c --- /dev/null +++ b/gen/gen_compile_info.rs @@ -0,0 +1,37 @@ +use std::path::PathBuf; + +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 +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(); + + 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()); + + let generated_code = template_code + .replace("{date}", &date) + .replace("{target}", &target) + .replace("{platform}", &platform) + .replace("{toolchain}", &toolchain) + .replace("{version}", &version) + .replace("{branch}", &branch) + .replace("{commit}", &commit); + + // Write the generated code + let compile_info_path = repo_root.join(COMPILE_INFO_RS); + tokio::fs::write(compile_info_path, generated_code) + .await + .unwrap(); +} diff --git a/gen/gen_iscc_script.rs b/gen/gen_iscc_script.rs new file mode 100644 index 0000000..1eddcca --- /dev/null +++ b/gen/gen_iscc_script.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; + +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) +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); + + let template = tokio::fs::read_to_string(&template_path).await.unwrap(); + + let author = get_author().unwrap(); + let version = get_version(); + let site = get_site().unwrap(); + + let generated = template + .replace("<<>>", &author) + .replace("<<>>", &version) + .replace("<<>>", &site); + + tokio::fs::write(output_path, generated).await.unwrap(); +} diff --git a/gen/gen_mod_files.rs b/gen/gen_mod_files.rs new file mode 100644 index 0000000..6e44eac --- /dev/null +++ b/gen/gen_mod_files.rs @@ -0,0 +1,96 @@ +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 new file mode 100644 index 0000000..2ac97bd --- /dev/null +++ b/gen/gen_override_renderer.rs @@ -0,0 +1,188 @@ +use std::{collections::HashSet, path::PathBuf}; + +use regex::Regex; +use tokio::fs; + +use crate::r#gen::{ + constants::{ + COMMANDS_PATH, OVERRIDE_RENDERER_ENTRY, OVERRIDE_RENDERER_ENTRY_TEMPLATE, TEMPLATE_END, + TEMPLATE_START, + }, + 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 = tokio::fs::read_to_string(&template_path).await.unwrap(); + + // Extract the template section from the template content + const MATCH_MARKER: &str = "// MATCHING"; + + let template_start_index = template + .find(TEMPLATE_START) + .ok_or("Template start marker not found") + .unwrap(); + let template_end_index = template + .find(TEMPLATE_END) + .ok_or("Template end marker not found") + .unwrap(); + + let template_slice = &template[template_start_index..template_end_index + TEMPLATE_END.len()]; + let renderer_template = template_slice + .trim_start_matches(TEMPLATE_START) + .trim_end_matches(TEMPLATE_END) + .trim_matches('\n'); + + // Generate the match arms for each renderer + let match_arms: String = all_possible_types + .iter() + .map(|type_name| { + let name = type_name.split("::").last().unwrap_or(type_name); + renderer_template + .replace("JVOutputTypeName", name) + .replace("JVOutputType", type_name) + .trim_matches('\n') + .to_string() + }) + .collect::>() + .join("\n"); + + // Replace the template section with the generated match arms + let final_content = template + .replace(renderer_template, "") + .replace(TEMPLATE_START, "") + .replace(TEMPLATE_END, "") + .replace(MATCH_MARKER, &match_arms) + .lines() + .filter(|line| !line.trim().is_empty()) + .collect::>() + .join("\n"); + + // Write the generated code + tokio::fs::write(output_path, final_content).await.unwrap(); +} + +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*\)").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 new file mode 100644 index 0000000..497d258 --- /dev/null +++ b/gen/gen_renderers_file.rs @@ -0,0 +1,97 @@ +use std::path::PathBuf; + +use crate::r#gen::constants::{ + OVERRIDE_RENDERER_DISPATCHER, OVERRIDE_RENDERER_DISPATCHER_TEMPLATE, REGISTRY_TOML, + TEMPLATE_END, TEMPLATE_START, +}; + +/// Generate renderer list file from Registry.toml configuration +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 = 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())); + } + + // Extract the template section from the template content + const MATCH_MARKER: &str = "// MATCH"; + + let template_start_index = template + .find(TEMPLATE_START) + .ok_or("Template start marker not found") + .unwrap(); + let template_end_index = template + .find(TEMPLATE_END) + .ok_or("Template end marker not found") + .unwrap(); + + let template_slice = &template[template_start_index..template_end_index + TEMPLATE_END.len()]; + let renderer_template = template_slice + .trim_start_matches(TEMPLATE_START) + .trim_end_matches(TEMPLATE_END) + .trim_matches('\n'); + + // Generate the match arms for each renderer + let match_arms: String = renderers + .iter() + .map(|(name, renderer_type)| { + renderer_template + .replace("<>", name) + .replace("RendererType", renderer_type) + .trim_matches('\n') + .to_string() + }) + .collect::>() + .join("\n"); + + // Replace the template section with the generated match arms + let final_content = template + .replace(renderer_template, "") + .replace(TEMPLATE_START, "") + .replace(TEMPLATE_END, "") + .replace(MATCH_MARKER, &match_arms) + .lines() + .filter(|line| !line.trim().is_empty()) + .collect::>() + .join("\n"); + + // Write the generated code + tokio::fs::write(output_path, final_content).await.unwrap(); + + println!( + "Generated renderer list file with {} renderers", + renderers.len() + ); +} diff --git a/gen/gen_specific_renderer.rs b/gen/gen_specific_renderer.rs new file mode 100644 index 0000000..0c66631 --- /dev/null +++ b/gen/gen_specific_renderer.rs @@ -0,0 +1,383 @@ +use std::{collections::HashMap, path::PathBuf}; + +use regex::Regex; + +use crate::r#gen::{ + constants::{ + RENDERERS_PATH, SPECIFIC_RENDERER_MATCHING, SPECIFIC_RENDERER_MATCHING_TEMPLATE, + TEMPLATE_END, TEMPLATE_START, + }, + resolve_types::resolve_type_paths, +}; + +const RENDERER_TYPE_PREFIX: &str = "crate::"; + +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 = tokio::fs::read_to_string(&template_path).await.unwrap(); + + // Extract the template section from the template content + const MATCH_MARKER: &str = "// MATCHING"; + + let template_start_index = template + .find(TEMPLATE_START) + .ok_or("Template start marker not found") + .unwrap(); + let template_end_index = template + .find(TEMPLATE_END) + .ok_or("Template end marker not found") + .unwrap(); + + let template_slice = &template[template_start_index..template_end_index + TEMPLATE_END.len()]; + let renderer_template = template_slice + .trim_start_matches(TEMPLATE_START) + .trim_end_matches(TEMPLATE_END) + .trim_matches('\n'); + + // Generate the match arms for each renderer + let match_arms: String = renderer_matches + .iter() + .map(|(renderer, output)| { + let output_name = output.split("::").last().unwrap_or(output); + renderer_template + .replace("OutputTypeName", output_name) + .replace("OutputType", output) + .replace("RendererType", renderer) + .trim_matches('\n') + .to_string() + }) + .collect::>() + .join("\n"); + + // Replace the template section with the generated match arms + let final_content = template + .replace(renderer_template, "") + .replace(TEMPLATE_START, "") + .replace(TEMPLATE_END, "") + .replace(MATCH_MARKER, &match_arms) + .lines() + .filter(|line| !line.trim().is_empty()) + .collect::>() + .join("\n"); + + // Write the generated code + tokio::fs::write(output_path, final_content).await.unwrap(); +} + +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 new file mode 100644 index 0000000..6079abc --- /dev/null +++ b/gen/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); + } + } + } +} diff --git a/macros/cmd_system_macros/Cargo.toml b/macros/cmd_system_macros/Cargo.toml new file mode 100644 index 0000000..4a91064 --- /dev/null +++ b/macros/cmd_system_macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cmd_system_macros" +version.workspace = true +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits", "visit"] } diff --git a/macros/cmd_system_macros/src/lib.rs b/macros/cmd_system_macros/src/lib.rs new file mode 100644 index 0000000..e585782 --- /dev/null +++ b/macros/cmd_system_macros/src/lib.rs @@ -0,0 +1,92 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Expr, ItemFn, Lit, Type, parse_macro_input, parse_quote}; + +#[proc_macro_attribute] +pub fn exec(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input_fn = parse_macro_input!(item as ItemFn); + let fn_block = &input_fn.block; + + let mut output_mappings = Vec::new(); + extract_cmd_output_macros(fn_block, &mut output_mappings); + + let mapping_fn = generate_mapping_function(&output_mappings); + + let expanded = quote! { + #input_fn + + #mapping_fn + }; + + TokenStream::from(expanded) +} + +fn extract_cmd_output_macros(block: &syn::Block, mappings: &mut Vec<(String, syn::Type)>) { + use syn::visit::Visit; + + struct CmdOutputVisitor<'a> { + mappings: &'a mut Vec<(String, syn::Type)>, + } + + impl<'a> syn::visit::Visit<'a> for CmdOutputVisitor<'a> { + fn visit_macro(&mut self, mac: &'a syn::Macro) { + if mac.path.is_ident("cmd_output") { + let nested_result = syn::parse2::(mac.tokens.clone()); + if let Ok(nested) = nested_result { + if nested.elems.len() < 2 { + syn::visit::visit_macro(self, mac); + return; + } + + let first_elem = &nested.elems[0]; + let second_elem = &nested.elems[1]; + + let type_path_opt = match first_elem { + Expr::Path(path) => Some(path), + _ => None, + }; + + let lit_str_opt = match second_elem { + Expr::Lit(lit) => match &lit.lit { + Lit::Str(lit_str) => Some(lit_str), + _ => None, + }, + _ => None, + }; + + if let (Some(type_path), Some(lit_str)) = (type_path_opt, lit_str_opt) { + let type_name = lit_str.value(); + if let Some(type_ident) = type_path.path.get_ident() { + let ty: Type = parse_quote!(#type_ident); + self.mappings.push((type_name, ty)); + } + } + } + } + + syn::visit::visit_macro(self, mac); + } + } + + let mut visitor = CmdOutputVisitor { mappings }; + visitor.visit_block(block); +} + +fn generate_mapping_function(mappings: &[(String, syn::Type)]) -> proc_macro2::TokenStream { + let mapping_entries: Vec<_> = mappings + .iter() + .map(|(name, ty)| { + quote! { + map.insert(#name.to_string(), std::any::TypeId::of::<#ty>()); + } + }) + .collect(); + + quote! { + fn get_output_type_mapping() -> std::collections::HashMap { + let mut map = std::collections::HashMap::new(); + #(#mapping_entries)* + map + } + } +} diff --git a/macros/render_system_macros/Cargo.toml b/macros/render_system_macros/Cargo.toml new file mode 100644 index 0000000..df435db --- /dev/null +++ b/macros/render_system_macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "render_system_macros" +version.workspace = true +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } diff --git a/macros/render_system_macros/src/lib.rs b/macros/render_system_macros/src/lib.rs new file mode 100644 index 0000000..7466b53 --- /dev/null +++ b/macros/render_system_macros/src/lib.rs @@ -0,0 +1,143 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{ItemFn, Type, parse_macro_input, spanned::Spanned}; + +/// Macro for simplifying renderer definitions +/// +/// Expands the `#[result_renderer(Renderer)]` macro into the corresponding struct and trait implementation +/// +/// # Example +/// ```ignore +/// #[result_renderer(MyRenderer)] +/// async fn render(data: &Output) -> Result { +/// // Rendering logic +/// } +/// ``` +/// +/// Expands to: +/// ```ignore +/// pub struct MyRenderer; +/// +/// impl JVResultRenderer for MyRenderer { +/// async fn render(data: &Output) -> Result { +/// // Rendering logic +/// } +/// } +/// +/// impl JVResultAutoRenderer for MyRenderer { +/// fn get_type_id(&self) -> std::any::TypeId { +/// std::any::TypeId::of::() +/// } +/// +/// fn get_data_type_id(&self) -> std::any::TypeId { +/// std::any::TypeId::of::() +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn result_renderer(args: TokenStream, input: TokenStream) -> TokenStream { + // Parse macro arguments (renderer struct name) + let renderer_name = parse_macro_input!(args as syn::Ident); + + // Parse the input function + let input_fn = parse_macro_input!(input as ItemFn); + + // Check if the function is async + if input_fn.sig.asyncness.is_none() { + return syn::Error::new(input_fn.sig.ident.span(), "renderer function must be async") + .to_compile_error() + .into(); + } + + // Get the function name + let fn_name = &input_fn.sig.ident; + + // Get function parameters + let fn_inputs = &input_fn.sig.inputs; + + // Check the number of function parameters + if fn_inputs.len() != 1 { + return syn::Error::new( + input_fn.sig.paren_token.span.join(), + "renderer function must have exactly one parameter", + ) + .to_compile_error() + .into(); + } + + // Extract the type of the first parameter + let param_type = match &fn_inputs[0] { + syn::FnArg::Typed(pat_type) => &pat_type.ty, + syn::FnArg::Receiver(_) => { + return syn::Error::new( + fn_inputs[0].span(), + "renderer function cannot have self parameter", + ) + .to_compile_error() + .into(); + } + }; + + // Check if the parameter type is a reference type, and extract the inner type + let inner_type = match &**param_type { + Type::Reference(type_ref) => { + // Ensure it's a reference type + &type_ref.elem + } + _ => { + return syn::Error::new( + param_type.span(), + "renderer function parameter must be a reference type (&Data)", + ) + .to_compile_error() + .into(); + } + }; + + // Extract the parameter pattern (for function calls) + let param_pattern = match &fn_inputs[0] { + syn::FnArg::Typed(pat_type) => &pat_type.pat, + _ => unreachable!(), + }; + + // Extract the function's visibility modifier + let visibility = &input_fn.vis; + + // Extract generic parameters (if any) + let generics = &input_fn.sig.generics; + + // Extract where clause (if any) + let where_clause = &generics.where_clause; + + // Build the output + let expanded = quote! { + #input_fn + + #visibility struct #renderer_name; + + impl #generics crate::systems::render::renderer::JVResultRenderer<#inner_type> for #renderer_name + #where_clause + { + fn render( + #fn_inputs + ) -> impl ::std::future::Future> + ::std::marker::Send + ::std::marker::Sync { + async move { + #fn_name(#param_pattern).await + } + } + + fn get_type_id(&self) -> std::any::TypeId { + std::any::TypeId::of::() + } + + fn get_data_type_id(&self) -> std::any::TypeId { + std::any::TypeId::of::<#inner_type>() + } + } + }; + + expanded.into() +} diff --git a/resources/locales/jvn/en.yml b/resources/locales/jvn/en.yml index ee386f2..8768e6a 100644 --- a/resources/locales/jvn/en.yml +++ b/resources/locales/jvn/en.yml @@ -28,9 +28,24 @@ process_error: An error occurred while parsing your command arguments! Please use `jv --help` to view help + renderer_override_but_request_help: | + Renderer override mode is enabled, but help output was requested. + This is not expected. + + Tips: When using `--renderer` to specify a renderer, do not use `--help`. + If you wish to suppress all output when an error occurs, use the `--no-error-logs` flag. + other: | %{error} + downcast_failed: | + Type conversion failed! + + This is usually a compilation-phase issue, not a user input error. + Please use `jv -v -C` to get detailed version traceback and contact the developers. + + github: https://github.com/JustEnoughVCS/CommandLine/ + prepare_error: io: | I/O error in preparation phase! @@ -95,3 +110,10 @@ render_error: renderer_not_found: | Renderer `%{renderer_name}` not found! + + type_mismatch: | + Render type mismatch! + This is usually a compilation-phase issue, not a user input error. + Please use `jv -v -C` to get detailed version traceback and contact the developers. + + github: https://github.com/JustEnoughVCS/CommandLine/ diff --git a/resources/locales/jvn/zh-CN.yml b/resources/locales/jvn/zh-CN.yml index f62ffb7..455ca90 100644 --- a/resources/locales/jvn/zh-CN.yml +++ b/resources/locales/jvn/zh-CN.yml @@ -27,9 +27,24 @@ process_error: 在将您的命令参数进行解析时出现错误! 请使用 `jv <命令> --help` 查看帮助 + renderer_override_but_request_help: | + 启用渲染器覆盖模式,但请求输出帮助信息 + 这不符合预期 + + 请在使用 `--renderer` 指定渲染器时,不要使用 `--help` + 若您希望在出现错误时,不输出任何内容,请使用 `--no-error-logs` 标识 + other: | %{error} + downcast_failed: | + 类型转换失败! + + 这通常是编译期造成的问题,而非用户的错误输入 + 请使用 `jv -v -C` 获得详细的版本追溯,并联系开发人员 + + github: https://github.com/JustEnoughVCS/CommandLine/ + prepare_error: io: | 命令在准备阶段发生了 I/O 错误! @@ -93,3 +108,10 @@ render_error: renderer_not_found: | 无法找到渲染器 `%{renderer_name}`! + + type_mismatch: | + 渲染类型不匹配! + 这通常是编译期造成的问题,而非用户的错误输入 + 请使用 `jv -v -C` 获得详细的版本追溯,并联系开发人员 + + github: https://github.com/JustEnoughVCS/CommandLine/ diff --git a/rust-analyzer.toml b/rust-analyzer.toml new file mode 100644 index 0000000..0a09afa --- /dev/null +++ b/rust-analyzer.toml @@ -0,0 +1,51 @@ +[package] +proc-macro.enable = true + +[cargo] +allFeatures = true +loadOutDirsFromCheck = true +runBuildScripts = true + +[rust-analyzer] +procMacro.enable = true +procMacro.attributes.enable = true + +diagnostics.disabled = ["unresolved-proc-macro"] + +inlayHints.typeHints = true +inlayHints.parameterHints = true +inlayHints.chainingHints = true + +completion.autoimport.enable = true +completion.postfix.enable = true + +lens.enable = true +lens.implementations.enable = true +lens.references.enable = true + +check.command = "clippy" +check.extraArgs = ["--all-features"] + +files.watcher = "client" + +macroExpansion.mode = "hir" +macroExpansion.maxDepth = 32 +macroExpansion.engines = { hir = true, tt = true } + +workspace.symbol.search.scope = "workspace" + +assist.importMergeBehavior = "last" +assist.importPrefix = "by_self" + +hover.actions.enable = true +hover.actions.debug.enable = true +hover.actions.gotoTypeDef.enable = true +hover.actions.implementations.enable = true +hover.actions.references.enable = true + +callInfo.full = true + +linkedProjects = ["Cargo.toml"] + +experimental.procAttrMacros = true +experimental.procMacro.server = true diff --git a/src/bin/jvn.rs b/src/bin/jvn.rs index 598be3d..1dbc517 100644 --- a/src/bin/jvn.rs +++ b/src/bin/jvn.rs @@ -3,7 +3,7 @@ use std::process::exit; use cli_utils::display::md; use cli_utils::env::current_locales; use cli_utils::levenshtein_distance::levenshtein_distance; -use just_enough_vcs_cli::systems::cmd::_registry::jv_cmd_nodes; +use just_enough_vcs_cli::systems::cmd::_commands::jv_cmd_nodes; use just_enough_vcs_cli::systems::cmd::cmd_system::JVCommandContext; use just_enough_vcs_cli::systems::cmd::errors::{CmdExecuteError, CmdPrepareError, CmdRenderError}; use just_enough_vcs_cli::systems::cmd::{errors::CmdProcessError, processer::jv_cmd_process}; @@ -120,6 +120,15 @@ async fn main() { eprintln!("{}", help) } } + CmdProcessError::RendererOverrideButRequestHelp => { + eprintln!( + "{}", + md(t!("process_error.renderer_override_but_request_help")) + ); + } + CmdProcessError::DowncastFailed => { + eprintln!("{}", md(t!("process_error.downcast_failed"))); + } } } std::process::exit(1); @@ -253,5 +262,11 @@ fn handle_render_error(cmd_render_error: CmdRenderError) { )) ); } + CmdRenderError::TypeMismatch { + expected: _, + actual: _, + } => { + eprintln!("{}", md(t!("render_error.type_mismatch"))); + } } } diff --git a/src/cmds.rs b/src/cmds.rs index 46057c8..92e587f 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -3,4 +3,5 @@ pub mod cmd; pub mod collect; pub mod r#in; pub mod out; +pub mod r#override; pub mod renderer; diff --git a/src/cmds/cmd/status.rs b/src/cmds/cmd/status.rs index f34dbb8..52f12d0 100644 --- a/src/cmds/cmd/status.rs +++ b/src/cmds/cmd/status.rs @@ -1,159 +1,156 @@ use std::{collections::HashMap, time::SystemTime}; -use just_enough_vcs::vcs::{ - constants::VAULT_HOST_NAME, data::local::workspace_analyzer::ModifiedRelativePathBuf, -}; - use crate::{ + cmd_output, cmds::{ arg::status::JVStatusArgument, collect::status::JVStatusCollect, r#in::status::JVStatusInput, out::status::{JVStatusOutput, JVStatusWrongModifyReason}, - renderer::status::JVStatusRenderer, }, systems::cmd::{ - cmd_system::{JVCommand, JVCommandContext}, + cmd_system::JVCommandContext, errors::{CmdExecuteError, CmdPrepareError}, workspace_reader::LocalWorkspaceReader, }, }; +use cmd_system_macros::exec; +use just_enough_vcs::vcs::{ + constants::VAULT_HOST_NAME, data::local::workspace_analyzer::ModifiedRelativePathBuf, +}; pub struct JVStatusCommand; +type Cmd = JVStatusCommand; +type Arg = JVStatusArgument; +type In = JVStatusInput; +type Collect = JVStatusCollect; -impl JVCommand - for JVStatusCommand -{ - async fn prepare( - _args: &JVStatusArgument, - _ctx: &JVCommandContext, - ) -> Result { - Ok(JVStatusInput) - } +fn help_str() -> String { + "".to_string() +} - async fn collect( - _args: &JVStatusArgument, - _ctx: &JVCommandContext, - ) -> Result { - // Initialize a reader for the local workspace and a default result structure - let mut reader = LocalWorkspaceReader::default(); - let mut collect = JVStatusCollect::default(); - - // Analyze the current status of the local workspace - // (detects changes like created, modified, moved, etc.) - let analyzed = reader.analyze_local_status().await?; - - // Retrieve the current account (member) ID - let account = reader.current_account().await?; - - // Retrieve the name of the current sheet - let sheet_name = reader.sheet_name().await?; - - // Is Host Mode - let is_host_mode = reader.is_host_mode().await?; - - let cached_sheet = reader.cached_sheet(&sheet_name).await?; - let sheet_holder = cached_sheet.holder().cloned().unwrap_or_default(); - let is_ref_sheet = sheet_holder == VAULT_HOST_NAME; - - // Get Latest file data - let latest_file_data = reader.pop_latest_file_data(&account).await?; - - // Get the timestamp of the last update, defaulting to the current time if not available - let update_time = reader - .latest_info() - .await? - .update_instant - .unwrap_or(SystemTime::now()); - - // Record the current system time - let now_time = SystemTime::now(); - - // Populate the result structure with the gathered data - collect.current_account = account; - collect.current_sheet = sheet_name; - collect.is_host_mode = is_host_mode; - collect.in_ref_sheet = is_ref_sheet; - collect.analyzed_result = analyzed; - collect.update_time = update_time; - collect.now_time = now_time; - collect.latest_file_data = latest_file_data; - Ok(collect) - } +async fn prepare(_args: &Arg, _ctx: &JVCommandContext) -> Result { + Ok(JVStatusInput) +} - async fn exec( - _input: JVStatusInput, - collect: JVStatusCollect, - ) -> Result { - let mut wrong_modified_items: HashMap = - HashMap::new(); +async fn collect(_args: &Arg, _ctx: &JVCommandContext) -> Result { + // Initialize a reader for the local workspace and a default result structure + let mut reader = LocalWorkspaceReader::default(); + let mut collect = JVStatusCollect::default(); + + // Analyze the current status of the local workspace + // (detects changes like created, modified, moved, etc.) + let analyzed = reader.analyze_local_status().await?; + + // Retrieve the current account (member) ID + let account = reader.current_account().await?; + + // Retrieve the name of the current sheet + let sheet_name = reader.sheet_name().await?; + + // Is Host Mode + let is_host_mode = reader.is_host_mode().await?; + + let cached_sheet = reader.cached_sheet(&sheet_name).await?; + let sheet_holder = cached_sheet.holder().cloned().unwrap_or_default(); + let is_ref_sheet = sheet_holder == VAULT_HOST_NAME; + + // Get Latest file data + let latest_file_data = reader.pop_latest_file_data(&account).await?; + + // Get the timestamp of the last update, defaulting to the current time if not available + let update_time = reader + .latest_info() + .await? + .update_instant + .unwrap_or(SystemTime::now()); + + // Record the current system time + let now_time = SystemTime::now(); + + // Populate the result structure with the gathered data + collect.current_account = account; + collect.current_sheet = sheet_name; + collect.is_host_mode = is_host_mode; + collect.in_ref_sheet = is_ref_sheet; + collect.analyzed_result = analyzed; + collect.update_time = update_time; + collect.now_time = now_time; + collect.latest_file_data = latest_file_data; + Ok(collect) +} - let latest_file_data = &collect.latest_file_data; +#[exec] +async fn exec( + _input: In, + collect: Collect, +) -> Result<(Box, String), CmdExecuteError> { + let mut wrong_modified_items: HashMap = + HashMap::new(); + + let latest_file_data = &collect.latest_file_data; + + // Calculate whether modifications are correc + let modified = &collect.analyzed_result.modified; + for item in modified { + // Get mapping + let Ok(mapping) = collect.local_sheet_data.mapping_data(&item) else { + continue; + }; - // Calculate whether modifications are correc - let modified = &collect.analyzed_result.modified; - for item in modified { - // Get mapping - let Ok(mapping) = collect.local_sheet_data.mapping_data(&item) else { + // Check base version + { + let base_version = mapping.version_when_updated().clone(); + let Some(latest_version) = latest_file_data + .file_version(mapping.mapping_vfid()) + .cloned() + else { continue; }; - // Check base version - { - let base_version = mapping.version_when_updated().clone(); - let Some(latest_version) = latest_file_data - .file_version(mapping.mapping_vfid()) - .cloned() - else { - continue; - }; - - // Base version dismatch - if base_version != latest_version { - wrong_modified_items.insert( - item.clone(), - JVStatusWrongModifyReason::BaseVersionMismatch { - base_version, - latest_version, - }, - ); - continue; - } - } - - // Check edit right (only check when current is not HOST) - if collect.current_account != VAULT_HOST_NAME { - let holder = latest_file_data.file_holder(mapping.mapping_vfid()); - if holder.is_none() { - wrong_modified_items.insert(item.clone(), JVStatusWrongModifyReason::NoHolder); - continue; - } - - let holder = holder.cloned().unwrap(); - if &collect.current_account != &holder { - wrong_modified_items.insert( - item.clone(), - JVStatusWrongModifyReason::ModifiedButNotHeld { holder: holder }, - ); - } + // Base version dismatch + if base_version != latest_version { + wrong_modified_items.insert( + item.clone(), + JVStatusWrongModifyReason::BaseVersionMismatch { + base_version, + latest_version, + }, + ); + continue; } } - let output = JVStatusOutput { - current_account: collect.current_account, - current_sheet: collect.current_sheet, - is_host_mode: collect.is_host_mode, - in_ref_sheet: collect.in_ref_sheet, - analyzed_result: collect.analyzed_result, - wrong_modified_items: wrong_modified_items, - update_time: collect.update_time, - now_time: collect.now_time, - }; + // Check edit right (only check when current is not HOST) + if collect.current_account != VAULT_HOST_NAME { + let holder = latest_file_data.file_holder(mapping.mapping_vfid()); + if holder.is_none() { + wrong_modified_items.insert(item.clone(), JVStatusWrongModifyReason::NoHolder); + continue; + } - Ok(output) + let holder = holder.cloned().unwrap(); + if &collect.current_account != &holder { + wrong_modified_items.insert( + item.clone(), + JVStatusWrongModifyReason::ModifiedButNotHeld { holder: holder }, + ); + } + } } - fn get_help_str() -> String { - "".to_string() - } + let output = JVStatusOutput { + current_account: collect.current_account, + current_sheet: collect.current_sheet, + is_host_mode: collect.is_host_mode, + in_ref_sheet: collect.in_ref_sheet, + analyzed_result: collect.analyzed_result, + wrong_modified_items: wrong_modified_items, + update_time: collect.update_time, + now_time: collect.now_time, + }; + + cmd_output!(output, JVStatusOutput) } + +crate::command_template!(); diff --git a/src/cmds/override.rs b/src/cmds/override.rs new file mode 100644 index 0000000..8cfd458 --- /dev/null +++ b/src/cmds/override.rs @@ -0,0 +1 @@ +pub mod renderer; diff --git a/src/cmds/override/renderer/json.rs b/src/cmds/override/renderer/json.rs new file mode 100644 index 0000000..b4e69f1 --- /dev/null +++ b/src/cmds/override/renderer/json.rs @@ -0,0 +1,17 @@ +use serde::Serialize; +use serde_json; + +use crate::{ + r_print, + systems::{cmd::errors::CmdRenderError, render::renderer::JVRenderResult}, +}; + +pub async fn render(data: &T) -> Result { + let mut r = JVRenderResult::default(); + let json_string = + serde_json::to_string(data).map_err(|e| CmdRenderError::SerializeFailed(e.to_string()))?; + + r_print!(r, "{}", json_string); + + Ok(r) +} diff --git a/src/cmds/override/renderer/json_pretty.rs b/src/cmds/override/renderer/json_pretty.rs new file mode 100644 index 0000000..3923117 --- /dev/null +++ b/src/cmds/override/renderer/json_pretty.rs @@ -0,0 +1,16 @@ +use serde::Serialize; + +use crate::{ + r_print, + systems::{cmd::errors::CmdRenderError, render::renderer::JVRenderResult}, +}; + +pub async fn render(data: &T) -> Result { + let mut r = JVRenderResult::default(); + let json_string = serde_json::to_string_pretty(data) + .map_err(|e| CmdRenderError::SerializeFailed(e.to_string()))?; + + r_print!(r, "{}", json_string); + + Ok(r) +} diff --git a/src/cmds/renderer/json.rs b/src/cmds/renderer/json.rs deleted file mode 100644 index 9a3105d..0000000 --- a/src/cmds/renderer/json.rs +++ /dev/null @@ -1,27 +0,0 @@ -use serde::Serialize; -use serde_json; - -use crate::{ - r_print, - systems::cmd::{ - errors::CmdRenderError, - renderer::{JVRenderResult, JVResultRenderer}, - }, -}; - -pub struct JVResultJsonRenderer; - -impl JVResultRenderer for JVResultJsonRenderer -where - T: Serialize + Sync, -{ - async fn render(data: &T) -> Result { - let mut r = JVRenderResult::default(); - let json_string = serde_json::to_string(data) - .map_err(|e| CmdRenderError::SerializeFailed(e.to_string()))?; - - r_print!(r, "{}", json_string); - - Ok(r) - } -} diff --git a/src/cmds/renderer/json_pretty.rs b/src/cmds/renderer/json_pretty.rs deleted file mode 100644 index a4a3ba5..0000000 --- a/src/cmds/renderer/json_pretty.rs +++ /dev/null @@ -1,26 +0,0 @@ -use serde::Serialize; - -use crate::{ - r_print, - systems::cmd::{ - errors::CmdRenderError, - renderer::{JVRenderResult, JVResultRenderer}, - }, -}; - -pub struct JVResultPrettyJsonRenderer; - -impl JVResultRenderer for JVResultPrettyJsonRenderer -where - T: Serialize + Sync, -{ - async fn render(data: &T) -> Result { - let mut r = JVRenderResult::default(); - let json_string = serde_json::to_string_pretty(data) - .map_err(|e| CmdRenderError::SerializeFailed(e.to_string()))?; - - r_print!(r, "{}", json_string); - - Ok(r) - } -} diff --git a/src/cmds/renderer/status.rs b/src/cmds/renderer/status.rs index 965ff87..573e74e 100644 --- a/src/cmds/renderer/status.rs +++ b/src/cmds/renderer/status.rs @@ -2,40 +2,36 @@ use cli_utils::{ display::{SimpleTable, md}, env::auto_update_outdate, }; +use render_system_macros::result_renderer; use rust_i18n::t; +use crate::cmds::out::status::JVStatusWrongModifyReason; use crate::{ - cmds::out::status::{JVStatusOutput, JVStatusWrongModifyReason}, + cmds::out::status::JVStatusOutput, r_println, - systems::cmd::{ - errors::CmdRenderError, - renderer::{JVRenderResult, JVResultRenderer}, - }, + systems::{cmd::errors::CmdRenderError, render::renderer::JVRenderResult}, }; -pub struct JVStatusRenderer; - enum Mode { StructuralChangesMode, ContentChangesMode, Clean, } -impl JVResultRenderer for JVStatusRenderer { - async fn render(data: &JVStatusOutput) -> Result { - let mut r = JVRenderResult::default(); +#[result_renderer(JVStatusRenderer)] +pub async fn render(data: &JVStatusOutput) -> Result { + let mut r = JVRenderResult::default(); - // Render Header - render_header(&mut r, data); + // Render Header + render_header(&mut r, data); - // Render Info and Mode - render_info_and_mode(&mut r, data); + // Render Info and Mode + render_info_and_mode(&mut r, data); - // Render Hint - render_hint(&mut r, data); + // Render Hint + render_hint(&mut r, data); - Ok(r) - } + Ok(r) } fn render_header(r: &mut JVRenderResult, data: &JVStatusOutput) { diff --git a/src/systems.rs b/src/systems.rs index 52958ec..7f77ca5 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1 +1,2 @@ pub mod cmd; +pub mod render; diff --git a/src/systems/cmd.rs b/src/systems/cmd.rs index ea8bbd7..ebfa4f1 100644 --- a/src/systems/cmd.rs +++ b/src/systems/cmd.rs @@ -1,6 +1,6 @@ -pub mod _registry; +pub mod _commands; pub mod cmd_system; pub mod errors; +pub mod macros; pub mod processer; -pub mod renderer; pub mod workspace_reader; diff --git a/src/systems/cmd/cmd_system.rs b/src/systems/cmd/cmd_system.rs index 20f5aef..030e711 100644 --- a/src/systems/cmd/cmd_system.rs +++ b/src/systems/cmd/cmd_system.rs @@ -1,64 +1,88 @@ -use serde::Serialize; - use crate::{ r_println, - systems::cmd::{ - errors::{CmdExecuteError, CmdPrepareError, CmdProcessError, CmdRenderError}, - renderer::{JVRenderResult, JVResultRenderer}, + systems::{ + cmd::errors::{CmdExecuteError, CmdPrepareError, CmdProcessError, CmdRenderError}, + render::{render_system::render, renderer::JVRenderResult}, }, }; -use std::future::Future; +use std::{ + any::{Any, TypeId}, + collections::HashMap, + future::Future, +}; pub struct JVCommandContext { pub help: bool, pub confirmed: bool, } -pub trait JVCommand +pub trait JVCommand where Argument: clap::Parser + Send, Input: Send, - Output: Serialize + Send + Sync, Collect: Send, - Renderer: JVResultRenderer + Send + Sync, { /// Get help string for the command fn get_help_str() -> String; - /// Process the command with a specified renderer, performing any necessary post-execution processing - fn process_with_renderer_flag( + /// Run the command and convert the result into type-agnostic serialized information, + /// then hand it over to the universal renderer for rendering. + /// Universal renderer: uses the renderer specified by the `--renderer` flag. + fn process_to_renderer_override( args: Vec, ctx: JVCommandContext, - renderer: String, - ) -> impl Future> + Send - where - Self: Sync, - { + renderer_override: String, + ) -> impl Future> + Send { async move { - let renderer_str = renderer.as_str(); - include!("_renderers.rs") + // If the `--help` flag is used, + // skip execution and return an error, + // unlike `process_to_render_system`, + // when the `--renderer` flag specifies a renderer, `--help` output is not allowed + if ctx.help { + return Err(CmdProcessError::RendererOverrideButRequestHelp); + } + + let (data, type_name) = Self::process(args, ctx).await?; + + let renderer_override = renderer_override.as_str(); + + // Serialize the data based on its concrete type + let render_result = include!("../render/_override_renderer_entry.rs"); + + match render_result { + Ok(r) => Ok(r), + Err(e) => Err(CmdProcessError::Render(e)), + } } } - /// performing any necessary post-execution processing - fn process( + /// Run the command and hand it over to the rendering system + /// to select the appropriate renderer for the result + fn process_to_render_system( args: Vec, ctx: JVCommandContext, - ) -> impl Future> + Send - where - Self: Sync, - { - Self::process_with_renderer::(args, ctx) + ) -> impl Future> + Send { + async { + // If the `--help` flag is used, + // skip execution and directly render help information + if ctx.help { + let mut r = JVRenderResult::default(); + r_println!(r, "{}", Self::get_help_str()); + return Ok(r); + } + + let (data, id_str) = Self::process(args, ctx).await?; + match render(data, id_str).await { + Ok(r) => Ok(r), + Err(e) => Err(CmdProcessError::Render(e)), + } + } } - /// Process the command output with a custom renderer, - /// performing any necessary post-execution processing - fn process_with_renderer + Send>( + fn process( args: Vec, ctx: JVCommandContext, - ) -> impl Future> + Send - where - Self: Sync, + ) -> impl Future, String), CmdProcessError>> + Send { async move { let mut full_args = vec!["jv".to_string()]; @@ -70,13 +94,6 @@ where Err(_) => return Err(CmdProcessError::ParseError(Self::get_help_str())), }; - // If the help flag is used, skip execution and directly print help - if ctx.help { - let mut r = JVRenderResult::default(); - r_println!(r, "{}", Self::get_help_str()); - return Ok(r); - } - let (input, collect) = match tokio::try_join!( Self::prepare(&parsed_args, &ctx), Self::collect(&parsed_args, &ctx) @@ -85,17 +102,15 @@ where Err(e) => return Err(CmdProcessError::from(e)), }; - let output = match Self::exec(input, collect).await { + let data = match Self::exec(input, collect).await { Ok(output) => output, Err(e) => return Err(CmdProcessError::from(e)), }; - match R::render(&output).await { - Ok(r) => Ok(r), - Err(e) => Err(CmdProcessError::from(e)), - } + Ok(data) } } + /// Prepare /// Converts Argument input into parameters readable during the execution phase fn prepare( @@ -116,5 +131,8 @@ where fn exec( input: Input, collect: Collect, - ) -> impl Future> + Send; + ) -> impl Future, String), CmdExecuteError>> + Send; + + /// Get output type mapping + fn get_output_type_mapping() -> HashMap; } diff --git a/src/systems/cmd/errors.rs b/src/systems/cmd/errors.rs index 358d15a..7ec5e1c 100644 --- a/src/systems/cmd/errors.rs +++ b/src/systems/cmd/errors.rs @@ -76,6 +76,12 @@ pub enum CmdRenderError { #[error("Renderer `{0}` not found")] RendererNotFound(String), + + #[error("Type mismatch: expected `{expected:?}`, got `{actual:?}`")] + TypeMismatch { + expected: std::any::TypeId, + actual: std::any::TypeId, + }, } impl CmdRenderError { @@ -109,6 +115,12 @@ pub enum CmdProcessError { #[error("Parse error")] ParseError(String), + + #[error("Renderer override mode is active, but user requested help")] + RendererOverrideButRequestHelp, + + #[error("Downcast failed")] + DowncastFailed, } impl CmdProcessError { diff --git a/src/systems/cmd/macros.rs b/src/systems/cmd/macros.rs new file mode 100644 index 0000000..c7d576d --- /dev/null +++ b/src/systems/cmd/macros.rs @@ -0,0 +1,166 @@ +#[macro_export] +/// # JVCS_CLI Command Definition Macro +/// +/// ## Import +/// +/// Add the following macro to your code +/// +/// ```ignore +/// crate::command_template!(); +/// ``` +/// +/// Then paste the following content into your code +/// +/// ```ignore +/// use cmd_system_macros::exec; +/// use crate::{ +/// cmd_output, +/// systems::cmd::{ +/// cmd_system::JVCommandContext, +/// errors::{CmdExecuteError, CmdPrepareError}, +/// workspace_reader::LocalWorkspaceReader, +/// }, +/// }; +/// +/// /// Define command type +/// /// Names should match the file name in the following format: +/// /// custom.rs matches JVCustomCommand, invoked using `jv custom ` +/// /// get_data.rs matches JVGetDataCommand, invoked using `jv get data ` +/// pub struct JVCustomCommand; +/// +/// /// Command type, should match the definition above +/// type Cmd = JVCustomCommand; +/// +/// /// Specify Argument +/// /// ```ignore +/// /// #[derive(Parser, Debug)] +/// /// pub struct JVCustomArgument; +/// /// ``` +/// type Arg = JVCustomArgument; +/// +/// /// Specify InputData +/// /// ```ignore +/// /// pub struct JVCustomInput; +/// /// ``` +/// type In = JVCustomInput; +/// +/// /// Specify CollectData +/// /// ```ignore +/// /// pub struct JVCustomCollect; +/// /// ``` +/// type Collect = JVCustomCollect; +/// +/// /// Return a string, rendered when the user needs help (command specifies `--help` or syntax error) +/// fn help_str() -> String { +/// todo!() +/// } +/// +/// /// Preparation phase, preprocess user input and convert to a data format friendly for the execution phase +/// async fn prepare(args: &Arg, ctx: &JVCommandContext) -> Result { +/// todo!() +/// } +/// +/// /// Collect necessary local information for execution +/// async fn collect(args: &Arg, ctx: &JVCommandContext) -> Result { +/// let reader = LocalWorkspaceReader::default(); +/// todo!() +/// } +/// +/// /// Execution phase, call core layer or other custom logic +/// #[exec] +/// async fn exec( +/// input: In, +/// collect: Collect, +/// ) -> Result<(Box, String), CmdExecuteError> { +/// todo!(); +/// +/// // Use the following method to return results +/// cmd_output!(output, JVCustomOutput) +/// } +/// ``` +/// +/// Of course, you can also use the comment-free version +/// +/// ```ignore +/// use cmd_system_macros::exec; +/// use crate::{ +/// cmd_output, +/// systems::cmd::{ +/// cmd_system::JVCommandContext, +/// errors::{CmdExecuteError, CmdPrepareError}, +/// workspace_reader::LocalWorkspaceReader, +/// }, +/// }; +/// +/// pub struct JVCustomCommand; +/// type Cmd = JVCustomCommand; +/// type Arg = JVCustomArgument; +/// type In = JVCustomInput; +/// type Collect = JVCustomCollect; +/// +/// fn help_str() -> String { +/// todo!() +/// } +/// +/// async fn prepare(args: &Arg, ctx: &JVCommandContext) -> Result { +/// todo!() +/// } +/// +/// async fn collect(args: &Arg, ctx: &JVCommandContext) -> Result { +/// let reader = LocalWorkspaceReader::default(); +/// todo!() +/// } +/// +/// #[exec] +/// async fn exec( +/// input: In, +/// collect: Collect, +/// ) -> Result<(Box, String), CmdExecuteError> { +/// todo!(); +/// cmd_output!(output, JVCustomOutput) +/// } +/// ``` +macro_rules! command_template { + () => { + impl $crate::systems::cmd::cmd_system::JVCommand for Cmd { + fn get_help_str() -> String { + help_str() + } + + async fn prepare( + args: &Arg, + ctx: &$crate::systems::cmd::cmd_system::JVCommandContext, + ) -> Result { + prepare(args, ctx).await + } + + async fn collect( + args: &Arg, + ctx: &$crate::systems::cmd::cmd_system::JVCommandContext, + ) -> Result { + collect(args, ctx).await + } + + async fn exec( + input: In, + collect: Collect, + ) -> Result< + (Box, String), + $crate::systems::cmd::errors::CmdExecuteError, + > { + exec(input, collect).await + } + + fn get_output_type_mapping() -> std::collections::HashMap { + get_output_type_mapping() + } + } + }; +} + +#[macro_export] +macro_rules! cmd_output { + ($v:expr, $t:ty) => { + Ok((Box::new($v), stringify!($t).to_string())) + }; +} diff --git a/src/systems/cmd/processer.rs b/src/systems/cmd/processer.rs index 7c464a2..4bcaaeb 100644 --- a/src/systems/cmd/processer.rs +++ b/src/systems/cmd/processer.rs @@ -1,7 +1,7 @@ -use crate::systems::cmd::_registry::{jv_cmd_nodes, jv_cmd_process_node}; +use crate::systems::cmd::_commands::{jv_cmd_nodes, jv_cmd_process_node}; use crate::systems::cmd::cmd_system::JVCommandContext; use crate::systems::cmd::errors::CmdProcessError; -use crate::systems::cmd::renderer::JVRenderResult; +use crate::systems::render::renderer::JVRenderResult; pub async fn jv_cmd_process( args: &Vec, diff --git a/src/systems/cmd/renderer.rs b/src/systems/cmd/renderer.rs deleted file mode 100644 index 1849ee9..0000000 --- a/src/systems/cmd/renderer.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use serde::Serialize; - -use crate::systems::cmd::errors::CmdRenderError; - -pub trait JVResultRenderer -where - Data: Serialize, -{ - fn render( - data: &Data, - ) -> impl Future> + Send + Sync; -} - -#[derive(Default, Debug, PartialEq)] -pub struct JVRenderResult { - render_text: String, -} - -impl Display for JVRenderResult { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}\n", self.render_text.trim()) - } -} - -impl JVRenderResult { - pub fn print(&mut self, text: &str) { - self.render_text.push_str(text); - } - - pub fn println(&mut self, text: &str) { - self.render_text.push_str(text); - self.render_text.push('\n'); - } - - pub fn clear(&mut self) { - self.render_text.clear(); - } -} - -#[macro_export] -macro_rules! r_print { - ($result:expr, $($arg:tt)*) => { - $result.print(&format!($($arg)*)) - }; -} - -#[macro_export] -macro_rules! r_println { - ($result:expr, $($arg:tt)*) => { - $result.println(&format!($($arg)*)) - }; -} diff --git a/src/systems/render.rs b/src/systems/render.rs new file mode 100644 index 0000000..8b387e7 --- /dev/null +++ b/src/systems/render.rs @@ -0,0 +1,2 @@ +pub mod render_system; +pub mod renderer; diff --git a/src/systems/render/render_system.rs b/src/systems/render/render_system.rs new file mode 100644 index 0000000..7371e7a --- /dev/null +++ b/src/systems/render/render_system.rs @@ -0,0 +1,14 @@ +use std::any::Any; + +use crate::systems::{ + cmd::errors::CmdRenderError, + render::renderer::{JVRenderResult, JVResultRenderer}, +}; + +pub async fn render( + data: Box, + type_name: String, +) -> Result { + let type_name_str = type_name.as_str(); + include!("_specific_renderer_matching.rs") +} diff --git a/src/systems/render/renderer.rs b/src/systems/render/renderer.rs new file mode 100644 index 0000000..9060683 --- /dev/null +++ b/src/systems/render/renderer.rs @@ -0,0 +1,53 @@ +use std::fmt::{Display, Formatter}; +use std::future::Future; + +use crate::systems::cmd::errors::CmdRenderError; + +pub trait JVResultRenderer { + fn render( + data: &Data, + ) -> impl Future> + Send + Sync; + + fn get_type_id(&self) -> std::any::TypeId; + fn get_data_type_id(&self) -> std::any::TypeId; +} + +#[derive(Default, Debug, PartialEq)] +pub struct JVRenderResult { + render_text: String, +} + +impl Display for JVRenderResult { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}\n", self.render_text.trim()) + } +} + +impl JVRenderResult { + pub fn print(&mut self, text: &str) { + self.render_text.push_str(text); + } + + pub fn println(&mut self, text: &str) { + self.render_text.push_str(text); + self.render_text.push('\n'); + } + + pub fn clear(&mut self) { + self.render_text.clear(); + } +} + +#[macro_export] +macro_rules! r_print { + ($result:expr, $($arg:tt)*) => { + $result.print(&format!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! r_println { + ($result:expr, $($arg:tt)*) => { + $result.println(&format!($($arg)*)) + }; +} diff --git a/templates/_commands.rs.template b/templates/_commands.rs.template new file mode 100644 index 0000000..84d2db4 --- /dev/null +++ b/templates/_commands.rs.template @@ -0,0 +1,40 @@ +// Auto generated by build.rs +use crate::systems::cmd::cmd_system::{JVCommand, JVCommandContext}; +use crate::systems::cmd::errors::CmdProcessError; +<> +/// Input parameters, execute a command node +pub async fn jv_cmd_process_node( + node: &str, + args: Vec, + ctx: JVCommandContext, + renderer_override: String +) -> Result { + match node { +// PROCESS +// -- TEMPLATE START -- + // Command `<>` + "<>" => { + if renderer_override == "default" { + return crate::<>::process_to_render_system( + args, ctx, + ) + .await; + } else { + return crate::<>::process_to_renderer_override( + args, + ctx, + renderer_override, + ) + .await; + } + } +// -- TEMPLATE END -- + _ => {} + } + return Err(CmdProcessError::NoNodeFound(node.to_string())); +} +<> +/// Get all command nodes +pub fn jv_cmd_nodes() -> Vec { + vec!<> +} diff --git a/templates/_override_renderer_dispatcher.rs.template b/templates/_override_renderer_dispatcher.rs.template new file mode 100644 index 0000000..64d2f40 --- /dev/null +++ b/templates/_override_renderer_dispatcher.rs.template @@ -0,0 +1,13 @@ +match renderer_override { +// MATCH +// -- TEMPLATE START -- + "<>" => { + RendererType::render(&concrete_data).await + } +// -- TEMPLATE END -- + _ => { + return Err(CmdProcessError::Render(CmdRenderError::RendererNotFound( + renderer_override.to_string(), + ))); + } +} diff --git a/templates/_override_renderer_entry.rs.template b/templates/_override_renderer_entry.rs.template new file mode 100644 index 0000000..06b2c35 --- /dev/null +++ b/templates/_override_renderer_entry.rs.template @@ -0,0 +1,13 @@ +// Auto generated by build.rs +match type_name.as_str() { +// MATCHING +// -- TEMPLATE START -- + "JVOutputTypeName" => { + let concrete_data = data + .downcast::() + .map_err(|_| CmdProcessError::DowncastFailed)?; + include!("../render/_override_renderer_dispatcher.rs") + } + _ => return Err(CmdProcessError::NoMatchingCommand), +// -- TEMPLATE END -- +} diff --git a/templates/_registry.rs.template b/templates/_registry.rs.template deleted file mode 100644 index 957484c..0000000 --- a/templates/_registry.rs.template +++ /dev/null @@ -1,33 +0,0 @@ -// Auto generated by build.rs -use crate::systems::cmd::cmd_system::{JVCommand, JVCommandContext}; -use crate::systems::cmd::errors::CmdProcessError; -<> -/// Input parameters, execute a command node -pub async fn jv_cmd_process_node( - node: &str, - args: Vec, - ctx: JVCommandContext, - renderer_override: String -) -> Result { - match node { -// PROCESS -// -- TEMPLATE START -- - // Command `<>` - "<>" => { - return crate::<>::process_with_renderer_flag( - args, - ctx, - renderer_override - ) - .await; - } -// -- TEMPLATE END -- - _ => {} - } - return Err(CmdProcessError::NoNodeFound(node.to_string())); -} -<> -/// Get all command nodes -pub fn jv_cmd_nodes() -> Vec { - vec!<> -} diff --git a/templates/_renderers.rs.template b/templates/_renderers.rs.template deleted file mode 100644 index 37f0f1b..0000000 --- a/templates/_renderers.rs.template +++ /dev/null @@ -1,16 +0,0 @@ -match renderer_str { -// MATCH -// -- TEMPLATE START -- - "<>" => { - Self::process_with_renderer::< - RendererType - >(args, ctx) - .await - } -// -- TEMPLATE END -- - _ => { - return Err(CmdProcessError::Render(CmdRenderError::RendererNotFound( - renderer_str.to_string(), - ))); - } -} diff --git a/templates/_specific_renderer_matching.rs.template b/templates/_specific_renderer_matching.rs.template new file mode 100644 index 0000000..9b3765f --- /dev/null +++ b/templates/_specific_renderer_matching.rs.template @@ -0,0 +1,14 @@ +match type_name_str { +// MATCHING +// -- TEMPLATE START -- + "OutputTypeName" => { + RendererType::render( + &data + .downcast::() + .unwrap(), + ) + .await + } +// -- TEMPLATE END -- + _ => Err(CmdRenderError::RendererNotFound(type_name)), +} -- cgit