summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-02-04 00:27:16 +0800
committer魏曹先生 <1992414357@qq.com>2026-02-04 00:27:16 +0800
commitd19e5d84ee21502fd3440511d4ffb1ee1f49d3b2 (patch)
treefb8efef6f8e9a26c5b60d4ac220b11d6c6f0775e
parent7ee0d3f20c875e7405bb8442c5eb0228d1599a03 (diff)
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
-rw-r--r--.cargo/registry.toml12
-rw-r--r--.gitignore1
-rw-r--r--CONTRIBUTING.md16
-rw-r--r--CONTRIBUTING_zh_CN.md16
-rw-r--r--Cargo.lock39
-rw-r--r--Cargo.toml17
-rw-r--r--build.rs593
-rw-r--r--gen.rs10
-rw-r--r--gen/constants.rs29
-rw-r--r--gen/env.rs136
-rw-r--r--gen/gen_commands_file.rs188
-rw-r--r--gen/gen_compile_info.rs37
-rw-r--r--gen/gen_iscc_script.rs25
-rw-r--r--gen/gen_mod_files.rs96
-rw-r--r--gen/gen_override_renderer.rs188
-rw-r--r--gen/gen_renderers_file.rs97
-rw-r--r--gen/gen_specific_renderer.rs383
-rw-r--r--gen/resolve_types.rs114
-rw-r--r--macros/cmd_system_macros/Cargo.toml12
-rw-r--r--macros/cmd_system_macros/src/lib.rs92
-rw-r--r--macros/render_system_macros/Cargo.toml12
-rw-r--r--macros/render_system_macros/src/lib.rs143
-rw-r--r--resources/locales/jvn/en.yml22
-rw-r--r--resources/locales/jvn/zh-CN.yml22
-rw-r--r--rust-analyzer.toml51
-rw-r--r--src/bin/jvn.rs17
-rw-r--r--src/cmds.rs1
-rw-r--r--src/cmds/cmd/status.rs253
-rw-r--r--src/cmds/override.rs1
-rw-r--r--src/cmds/override/renderer/json.rs17
-rw-r--r--src/cmds/override/renderer/json_pretty.rs16
-rw-r--r--src/cmds/renderer/json.rs27
-rw-r--r--src/cmds/renderer/json_pretty.rs26
-rw-r--r--src/cmds/renderer/status.rs32
-rw-r--r--src/systems.rs1
-rw-r--r--src/systems/cmd.rs4
-rw-r--r--src/systems/cmd/cmd_system.rs106
-rw-r--r--src/systems/cmd/errors.rs12
-rw-r--r--src/systems/cmd/macros.rs166
-rw-r--r--src/systems/cmd/processer.rs4
-rw-r--r--src/systems/render.rs2
-rw-r--r--src/systems/render/render_system.rs14
-rw-r--r--src/systems/render/renderer.rs (renamed from src/systems/cmd/renderer.rs)11
-rw-r--r--templates/_commands.rs.template (renamed from templates/_registry.rs.template)21
-rw-r--r--templates/_override_renderer_dispatcher.rs.template (renamed from templates/_renderers.rs.template)9
-rw-r--r--templates/_override_renderer_entry.rs.template13
-rw-r--r--templates/_specific_renderer_matching.rs.template14
47 files changed, 2276 insertions, 842 deletions
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
@@ -410,6 +410,15 @@ dependencies = [
]
[[package]]
+name = "cmd_system_macros"
+version = "0.1.0-dev"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -682,6 +691,17 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -1452,6 +1476,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2054,6 +2087,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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<dyn std::error::Error>> {
- 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>>>", &author)
- .replace("<<<VERSION>>>", &version)
- .replace("<<<SITE>>>", &site);
-
- std::fs::write(output_path, generated)?;
- Ok(())
-}
-
-fn get_author() -> Result<String, Box<dyn std::error::Error>> {
- let cargo_toml_path = std::path::Path::new("Cargo.toml");
- let cargo_toml_content = std::fs::read_to_string(cargo_toml_path)?;
- let cargo_toml: toml::Value = toml::from_str(&cargo_toml_content)?;
-
- if let Some(package) = cargo_toml.get("package") {
- if let Some(authors) = package.get("authors") {
- if let Some(authors_array) = authors.as_array() {
- if let Some(first_author) = authors_array.get(0) {
- if let Some(author_str) = first_author.as_str() {
- return Ok(author_str.to_string());
- }
- }
- }
- }
- }
-
- Err("Author not found in Cargo.toml".into())
-}
-
-fn get_site() -> Result<String, Box<dyn std::error::Error>> {
- let cargo_toml_path = std::path::Path::new("Cargo.toml");
- let cargo_toml_content = std::fs::read_to_string(cargo_toml_path)?;
- let cargo_toml: toml::Value = toml::from_str(&cargo_toml_content)?;
-
- if let Some(package) = cargo_toml.get("package") {
- if let Some(homepage) = package.get("homepage") {
- if let Some(site_str) = homepage.as_str() {
- return Ok(site_str.to_string());
- }
- }
- }
-
- Err("Homepage not found in Cargo.toml".into())
-}
-
-/// Generate compile info
-fn generate_compile_info(repo_root: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
- // 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<String, Box<dyn std::error::Error>> {
- let output = Command::new("git")
- .args(["branch", "--show-current"])
- .output()?;
-
- if output.status.success() {
- let branch = String::from_utf8(output.stdout)?.trim().to_string();
-
- if branch.is_empty() {
- // Try to get HEAD reference if no branch (detached HEAD)
- let output = Command::new("git")
- .args(["rev-parse", "--abbrev-ref", "HEAD"])
- .output()?;
-
- if output.status.success() {
- let head_ref = String::from_utf8(output.stdout)?.trim().to_string();
- return Ok(head_ref);
- }
- } else {
- return Ok(branch);
- }
- }
-
- Err("Failed to get git branch".into())
-}
-
-/// Get current git commit hash
-fn get_git_commit() -> Result<String, Box<dyn std::error::Error>> {
- let output = Command::new("git").args(["rev-parse", "HEAD"]).output()?;
-
- if output.status.success() {
- let commit = String::from_utf8(output.stdout)?.trim().to_string();
- return Ok(commit);
- }
-
- Err("Failed to get git commit".into())
-}
-
-/// Generate registry file from Registry.toml configuration
-fn generate_cmd_registry_file(repo_root: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
- 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 = "<<LINE>>";
- const NODES: &str = "<<NODES>>";
-
- 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>>", key)
- .replace("<<NODE_NAME>>", node)
- .replace("<<COMMAND_TYPE>>", cmd_type)
- .trim_matches('\n')
- .to_string()
- })
- .collect::<Vec<_>>()
- .join("\n");
-
- let nodes_str = format!(
- "[\n {}\n ]",
- nodes
- .iter()
- .map(|node| format!("\"{}\".to_string()", node))
- .collect::<Vec<_>>()
- .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::<Vec<_>>()
- .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<dyn std::error::Error>> {
- 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>>", name)
- .replace("RendererType", renderer_type)
- .trim_matches('\n')
- .to_string()
})
- .collect::<Vec<String>>()
- .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::<Vec<_>>()
- .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<dyn std::error::Error>> {
- // 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<String, Box<dyn std::error::Error>> {
+ let cargo_toml_path = std::path::Path::new("Cargo.toml");
+ let cargo_toml_content = std::fs::read_to_string(cargo_toml_path)?;
+ let cargo_toml: toml::Value = toml::from_str(&cargo_toml_content)?;
+
+ if let Some(package) = cargo_toml.get("package") {
+ if let Some(authors) = package.get("authors") {
+ if let Some(authors_array) = authors.as_array() {
+ if let Some(first_author) = authors_array.get(0) {
+ if let Some(author_str) = first_author.as_str() {
+ return Ok(author_str.to_string());
+ }
+ }
+ }
+ }
+ }
+
+ Err("Author not found in Cargo.toml".into())
+}
+
+pub fn get_site() -> Result<String, Box<dyn std::error::Error>> {
+ let cargo_toml_path = std::path::Path::new("Cargo.toml");
+ let cargo_toml_content = std::fs::read_to_string(cargo_toml_path)?;
+ let cargo_toml: toml::Value = toml::from_str(&cargo_toml_content)?;
+
+ if let Some(package) = cargo_toml.get("package") {
+ if let Some(homepage) = package.get("homepage") {
+ if let Some(site_str) = homepage.as_str() {
+ return Ok(site_str.to_string());
+ }
+ }
+ }
+
+ Err("Homepage not found in Cargo.toml".into())
+}
+
+pub fn get_platform(target: &str) -> String {
+ if target.contains("windows") {
+ "Windows".to_string()
+ } else if target.contains("linux") {
+ "Linux".to_string()
+ } else if target.contains("darwin") || target.contains("macos") {
+ "macOS".to_string()
+ } else if target.contains("android") {
+ "Android".to_string()
+ } else if target.contains("ios") {
+ "iOS".to_string()
+ } else {
+ "Unknown".to_string()
+ }
+}
+
+pub fn get_toolchain() -> String {
+ let rustc_version = std::process::Command::new("rustc")
+ .arg("--version")
+ .output()
+ .ok()
+ .and_then(|output| String::from_utf8(output.stdout).ok())
+ .unwrap_or_else(|| "unknown".to_string())
+ .trim()
+ .to_string();
+
+ let channel = if rustc_version.contains("nightly") {
+ "nightly"
+ } else if rustc_version.contains("beta") {
+ "beta"
+ } else {
+ "stable"
+ };
+
+ format!("{} ({})", rustc_version, channel)
+}
+
+pub fn get_version() -> String {
+ let cargo_toml_path = std::path::Path::new("Cargo.toml");
+ let cargo_toml_content = match std::fs::read_to_string(cargo_toml_path) {
+ Ok(content) => content,
+ Err(_) => return "unknown".to_string(),
+ };
+
+ let cargo_toml: toml::Value = match toml::from_str(&cargo_toml_content) {
+ Ok(value) => value,
+ Err(_) => return "unknown".to_string(),
+ };
+
+ if let Some(workspace) = cargo_toml.get("workspace") {
+ if let Some(package) = workspace.get("package") {
+ if let Some(version) = package.get("version") {
+ if let Some(version_str) = version.as_str() {
+ return version_str.to_string();
+ }
+ }
+ }
+ }
+
+ "unknown".to_string()
+}
+
+pub fn get_git_branch() -> Result<String, Box<dyn std::error::Error>> {
+ let output = Command::new("git")
+ .args(["branch", "--show-current"])
+ .output()?;
+
+ if output.status.success() {
+ let branch = String::from_utf8(output.stdout)?.trim().to_string();
+
+ if branch.is_empty() {
+ // Try to get HEAD reference if no branch (detached HEAD)
+ let output = Command::new("git")
+ .args(["rev-parse", "--abbrev-ref", "HEAD"])
+ .output()?;
+
+ if output.status.success() {
+ let head_ref = String::from_utf8(output.stdout)?.trim().to_string();
+ return Ok(head_ref);
+ }
+ } else {
+ return Ok(branch);
+ }
+ }
+
+ Err("Failed to get git branch".into())
+}
+
+pub fn get_git_commit() -> Result<String, Box<dyn std::error::Error>> {
+ let output = Command::new("git").args(["rev-parse", "HEAD"]).output()?;
+
+ if output.status.success() {
+ let commit = String::from_utf8(output.stdout)?.trim().to_string();
+ return Ok(commit);
+ }
+
+ Err("Failed to get git commit".into())
+}
diff --git a/gen/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 = "<<LINE>>";
+ const NODES: &str = "<<NODES>>";
+
+ 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>>", key)
+ .replace("<<NODE_NAME>>", node)
+ .replace("<<COMMAND_TYPE>>", cmd_type)
+ .trim_matches('\n')
+ .to_string()
+ })
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ let nodes_str = format!(
+ "[\n {}\n ]",
+ nodes
+ .iter()
+ .map(|node| format!("\"{}\".to_string()", node))
+ .collect::<Vec<_>>()
+ .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::<Vec<_>>()
+ .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>>>", &author)
+ .replace("<<<VERSION>>>", &version)
+ .replace("<<<SITE>>>", &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::<Vec<String>>()
+ .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::<Vec<_>>()
+ .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<String> {
+ let mut all_types = HashSet::new();
+ let mut dirs_to_visit = vec![dir.clone()];
+
+ while let Some(current_dir) = dirs_to_visit.pop() {
+ let entries_result = fs::read_dir(&current_dir).await;
+ if entries_result.is_err() {
+ continue;
+ }
+
+ let mut entries = entries_result.unwrap();
+
+ loop {
+ let entry_result = entries.next_entry().await;
+ if entry_result.is_err() {
+ break;
+ }
+
+ let entry_opt = entry_result.unwrap();
+ if entry_opt.is_none() {
+ break;
+ }
+
+ let entry = entry_opt.unwrap();
+ let path = entry.path();
+
+ if path.is_dir() {
+ dirs_to_visit.push(path);
+ continue;
+ }
+
+ let is_rs_file = path.extension().map(|ext| ext == "rs").unwrap_or(false);
+
+ if !is_rs_file {
+ continue;
+ }
+
+ let code_result = fs::read_to_string(&path).await;
+ if code_result.is_err() {
+ continue;
+ }
+
+ let code = code_result.unwrap();
+ let types_opt = resolve_type_paths(&code, get_output_types(&code).unwrap());
+
+ if let Some(types) = types_opt {
+ for type_name in types {
+ all_types.insert(type_name);
+ }
+ }
+ }
+ }
+
+ all_types
+}
+
+pub fn get_output_types(code: &String) -> Option<Vec<String>> {
+ let mut output_types = Vec::new();
+
+ // Find all cmd_output! macros
+ let cmd_output_re = Regex::new(r"cmd_output!\s*\(\s*[^,]+,\s*([^)]+)\s*\)").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>>", name)
+ .replace("RendererType", renderer_type)
+ .trim_matches('\n')
+ .to_string()
+ })
+ .collect::<Vec<String>>()
+ .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::<Vec<_>>()
+ .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<RendererTypeFullName, OutputTypeFullName>
+ let mut renderer_matches: HashMap<String, String> = HashMap::new();
+
+ let renderer_path = repo_root.join(RENDERERS_PATH);
+
+ collect_renderers(&renderer_path, &mut renderer_matches);
+
+ let template_path = repo_root.join(SPECIFIC_RENDERER_MATCHING_TEMPLATE);
+ let output_path = repo_root.join(SPECIFIC_RENDERER_MATCHING);
+
+ // Read the template
+ let template = 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::<Vec<String>>()
+ .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::<Vec<_>>()
+ .join("\n");
+
+ // Write the generated code
+ tokio::fs::write(output_path, final_content).await.unwrap();
+}
+
+fn collect_renderers(dir_path: &PathBuf, matches: &mut HashMap<String, String>) {
+ if let Ok(entries) = std::fs::read_dir(dir_path) {
+ for entry in entries {
+ if let Ok(entry) = entry {
+ let path = entry.path();
+ if path.is_dir() {
+ collect_renderers(&path, matches);
+ } else if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") {
+ process_rs_file(&path, matches);
+ }
+ }
+ }
+ }
+}
+
+fn process_rs_file(file_path: &PathBuf, matches: &mut HashMap<String, String>) {
+ let content = match std::fs::read_to_string(file_path) {
+ Ok(content) => content,
+ Err(_) => return,
+ };
+
+ let renderer_info = match get_renderer_types(&content) {
+ Some(info) => info,
+ None => return,
+ };
+
+ let (renderer_type, output_type) = renderer_info;
+
+ let full_renderer_type = build_full_renderer_type(file_path, &renderer_type);
+ let full_output_type = resolve_type_paths(&content, vec![output_type])
+ .unwrap()
+ .get(0)
+ .unwrap()
+ .clone();
+
+ matches.insert(full_renderer_type, full_output_type);
+}
+
+fn build_full_renderer_type(file_path: &PathBuf, renderer_type: &str) -> String {
+ let relative_path = file_path
+ .strip_prefix(std::env::current_dir().unwrap())
+ .unwrap_or(file_path);
+ let relative_path = relative_path.with_extension("");
+ let path_str = relative_path.to_string_lossy();
+
+ // Normalize path separators and remove "./" prefix if present
+ let normalized_path = path_str
+ .replace('\\', "/")
+ .trim_start_matches("./")
+ .to_string();
+
+ let mut module_path = normalized_path.split('/').collect::<Vec<&str>>().join("::");
+
+ if module_path.starts_with("src") {
+ module_path = module_path.trim_start_matches("src").to_string();
+ if module_path.starts_with("::") {
+ module_path = module_path.trim_start_matches("::").to_string();
+ }
+ }
+
+ format!("{}{}::{}", RENDERER_TYPE_PREFIX, module_path, renderer_type)
+}
+
+pub fn get_renderer_types(code: &String) -> Option<(String, String)> {
+ let renderer_re = Regex::new(r"#\[result_renderer\(([^)]+)\)\]").unwrap();
+
+ let func_re =
+ Regex::new(r"(?:pub\s+)?(?:async\s+)?fn\s+\w+\s*\(\s*(?:mut\s+)?\w+\s*:\s*&([^),]+)\s*")
+ .unwrap();
+
+ let code_without_comments = code
+ .lines()
+ .filter(|line| !line.trim_start().starts_with("//"))
+ .collect::<Vec<&str>>()
+ .join("\n");
+
+ let renderer_captures = renderer_re.captures(&code_without_comments);
+ let func_captures = func_re.captures(&code_without_comments);
+
+ match (renderer_captures, func_captures) {
+ (Some(renderer_cap), Some(func_cap)) => {
+ let renderer_type = renderer_cap[1].trim().to_string();
+ let output_type = func_cap[1].trim().to_string();
+ Some((renderer_type, output_type))
+ }
+ _ => None,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test1() {
+ const SITUATION: &str = "
+ #[result_renderer(MyRenderer)]
+ pub async fn render(data: &SomeOutput) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MyRenderer");
+ assert_eq!(output, "SomeOutput");
+ }
+
+ #[test]
+ fn test2() {
+ const SITUATION: &str = "
+ #[result_renderer(MyRenderer)]
+ pub async fn some_render(output: &SomeOutput) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MyRenderer");
+ assert_eq!(output, "SomeOutput");
+ }
+
+ #[test]
+ fn test3() {
+ const SITUATION: &str = "
+ #[result_renderer(MyRenderer)]
+ async fn some_render(output: &SomeOutput) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MyRenderer");
+ assert_eq!(output, "SomeOutput");
+ }
+
+ #[test]
+ fn test4() {
+ const SITUATION: &str = "
+ #[result_renderer(MyRenderer)]
+ async pub fn some_render(output: &SomeOutput) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MyRenderer");
+ assert_eq!(output, "SomeOutput");
+ }
+
+ #[test]
+ fn test5() {
+ const SITUATION: &str = "
+ #[result_renderer(MyRenderer)]
+ fn some_render(output: &SomeOutput2) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MyRenderer");
+ assert_eq!(output, "SomeOutput2");
+ }
+
+ #[test]
+ fn test6() {
+ const SITUATION: &str = "
+ #[result__renderer(MyRenderer)]
+ fn some_render(output: &SomeOutput2) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(
+ result.is_none(),
+ "Should fail to parse when annotation doesn't match"
+ );
+ }
+
+ #[test]
+ fn test7() {
+ const SITUATION: &str = "
+ #[result_renderer(MyRenderer)]
+ fn some_render() -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(
+ result.is_none(),
+ "Should fail to parse when no function parameter"
+ );
+ }
+
+ #[test]
+ fn test8() {
+ const SITUATION: &str = "
+ #[result_renderer(MyRenderer)]
+ fn some_render(output: &SomeOutput, context: &Context) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MyRenderer");
+ assert_eq!(output, "SomeOutput");
+ }
+
+ #[test]
+ fn test9() {
+ const SITUATION: &str = "
+ #[result_renderer(MyRenderer<T>)]
+ fn some_render(output: &SomeOutput<T>) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MyRenderer<T>");
+ assert_eq!(output, "SomeOutput<T>");
+ }
+
+ #[test]
+ fn test10() {
+ const SITUATION: &str = "
+ #[result_renderer(MyRenderer<'a>)]
+ fn some_render(output: &SomeOutput<'a>) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MyRenderer<'a>");
+ assert_eq!(output, "SomeOutput<'a>");
+ }
+
+ #[test]
+ fn test11() {
+ const SITUATION: &str = "
+ #[result_renderer( MyRenderer )]
+ fn some_render( output : & SomeOutput ) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MyRenderer");
+ assert_eq!(output, "SomeOutput");
+ }
+
+ #[test]
+ fn test12() {
+ const SITUATION: &str = "
+ #[result_renderer(AnotherRenderer)]
+ fn some_render(output: &DifferentOutput) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "AnotherRenderer");
+ assert_eq!(output, "DifferentOutput");
+ }
+
+ #[test]
+ fn test13() {
+ const SITUATION: &str = "
+ // #[result_renderer(WrongRenderer)]
+ #[result_renderer(CorrectRenderer)]
+ fn some_render(output: &CorrectOutput) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "CorrectRenderer");
+ assert_eq!(output, "CorrectOutput");
+ }
+
+ #[test]
+ fn test14() {
+ const SITUATION: &str = "
+ #[result_renderer(MultiLineRenderer)]
+ fn some_render(
+ output: &MultiLineOutput
+ ) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MultiLineRenderer");
+ assert_eq!(output, "MultiLineOutput");
+ }
+
+ #[test]
+ fn test15() {
+ const SITUATION: &str = "
+ #[result_renderer(MutRenderer)]
+ fn some_render(mut output: &MutOutput) -> Result<JVRenderResult, CmdRenderError>
+ ";
+
+ let result = get_renderer_types(&SITUATION.to_string());
+ assert!(result.is_some(), "Parse failed");
+ let (renderer, output) = result.unwrap();
+ assert_eq!(renderer, "MutRenderer");
+ assert_eq!(output, "MutOutput");
+ }
+}
diff --git a/gen/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<String>) -> Option<Vec<String>> {
+ let mut type_mappings = std::collections::HashMap::new();
+
+ // Extract all use statements
+ let use_re = Regex::new(r"use\s+([^;]*(?:\{[^}]*\}[^;]*)*);").ok()?;
+ let mut use_statements = Vec::new();
+ for cap in use_re.captures_iter(&code) {
+ use_statements.push(cap[1].to_string());
+ }
+
+ // Process each use statement to build type mappings
+ for stmt in &use_statements {
+ let stmt = stmt.trim();
+
+ if stmt.contains("::{") {
+ if let Some(pos) = stmt.find("::{") {
+ let base_path = &stmt[..pos];
+ let content = &stmt[pos + 3..stmt.len() - 1];
+ process_nested_use(base_path, content, &mut type_mappings);
+ }
+ } else {
+ // Process non-nested use statements
+ if let Some(pos) = stmt.rfind("::") {
+ let type_name = &stmt[pos + 2..];
+ type_mappings.insert(type_name.to_string(), stmt.to_string());
+ } else {
+ type_mappings.insert(stmt.to_string(), stmt.to_string());
+ }
+ }
+ }
+
+ // Resolve type names to full paths
+ let mut result = Vec::new();
+ for type_name in type_names {
+ if let Some(full_path) = type_mappings.get(&type_name) {
+ result.push(full_path.clone());
+ }
+ }
+
+ Some(result)
+}
+
+fn process_nested_use(
+ base_path: &str,
+ content: &str,
+ mappings: &mut std::collections::HashMap<String, String>,
+) {
+ let mut items = Vec::new();
+ let mut current_item = String::new();
+ let mut brace_depth = 0;
+
+ // Split nested content
+ for c in content.chars() {
+ match c {
+ '{' => {
+ brace_depth += 1;
+ current_item.push(c);
+ }
+ '}' => {
+ brace_depth -= 1;
+ current_item.push(c);
+ }
+ ',' => {
+ if brace_depth == 0 {
+ items.push(current_item.trim().to_string());
+ current_item.clear();
+ } else {
+ current_item.push(c);
+ }
+ }
+ _ => {
+ current_item.push(c);
+ }
+ }
+ }
+
+ if !current_item.trim().is_empty() {
+ items.push(current_item.trim().to_string());
+ }
+
+ // Process each item
+ for item in items {
+ if item.is_empty() {
+ continue;
+ }
+
+ if item.contains("::{") {
+ if let Some(pos) = item.find("::{") {
+ let sub_path = &item[..pos];
+ let sub_content = &item[pos + 3..item.len() - 1];
+ let new_base = if base_path.is_empty() {
+ sub_path.to_string()
+ } else {
+ format!("{}::{}", base_path, sub_path)
+ };
+ process_nested_use(&new_base, sub_content, mappings);
+ }
+ } else {
+ let full_path = if base_path.is_empty() {
+ item.to_string()
+ } else {
+ format!("{}::{}", base_path, item)
+ };
+ if let Some(pos) = item.rfind("::") {
+ let type_name = &item[pos + 2..];
+ mappings.insert(type_name.to_string(), full_path);
+ } else {
+ mappings.insert(item.to_string(), full_path);
+ }
+ }
+ }
+}
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::<syn::ExprTuple>(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<String, std::any::TypeId> {
+ 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<JVRenderResult, CmdRenderError> {
+/// // Rendering logic
+/// }
+/// ```
+///
+/// Expands to:
+/// ```ignore
+/// pub struct MyRenderer;
+///
+/// impl JVResultRenderer<Output> for MyRenderer {
+/// async fn render(data: &Output) -> Result<JVRenderResult, CmdRenderError> {
+/// // Rendering logic
+/// }
+/// }
+///
+/// impl JVResultAutoRenderer<Output> for MyRenderer {
+/// fn get_type_id(&self) -> std::any::TypeId {
+/// std::any::TypeId::of::<Self>()
+/// }
+///
+/// fn get_data_type_id(&self) -> std::any::TypeId {
+/// std::any::TypeId::of::<Output>()
+/// }
+/// }
+/// ```
+#[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<Output = ::std::result::Result<
+ crate::systems::render::renderer::JVRenderResult,
+ crate::systems::cmd::errors::CmdRenderError
+ >> + ::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::<Self>()
+ }
+
+ 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 <command> --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<JVStatusArgument, JVStatusInput, JVStatusCollect, JVStatusOutput, JVStatusRenderer>
- for JVStatusCommand
-{
- async fn prepare(
- _args: &JVStatusArgument,
- _ctx: &JVCommandContext,
- ) -> Result<JVStatusInput, CmdPrepareError> {
- Ok(JVStatusInput)
- }
+fn help_str() -> String {
+ "".to_string()
+}
- async fn collect(
- _args: &JVStatusArgument,
- _ctx: &JVCommandContext,
- ) -> Result<JVStatusCollect, CmdPrepareError> {
- // 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<In, CmdPrepareError> {
+ Ok(JVStatusInput)
+}
- async fn exec(
- _input: JVStatusInput,
- collect: JVStatusCollect,
- ) -> Result<JVStatusOutput, CmdExecuteError> {
- let mut wrong_modified_items: HashMap<ModifiedRelativePathBuf, JVStatusWrongModifyReason> =
- HashMap::new();
+async fn collect(_args: &Arg, _ctx: &JVCommandContext) -> Result<Collect, CmdPrepareError> {
+ // 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<dyn std::any::Any + Send + 'static>, String), CmdExecuteError> {
+ let mut wrong_modified_items: HashMap<ModifiedRelativePathBuf, JVStatusWrongModifyReason> =
+ 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<T: Serialize + Send>(data: &T) -> Result<JVRenderResult, CmdRenderError> {
+ 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<T: Serialize + Send>(data: &T) -> Result<JVRenderResult, CmdRenderError> {
+ 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<T> JVResultRenderer<T> for JVResultJsonRenderer
-where
- T: Serialize + Sync,
-{
- async fn render(data: &T) -> Result<JVRenderResult, CmdRenderError> {
- 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<T> JVResultRenderer<T> for JVResultPrettyJsonRenderer
-where
- T: Serialize + Sync,
-{
- async fn render(data: &T) -> Result<JVRenderResult, CmdRenderError> {
- 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<JVStatusOutput> for JVStatusRenderer {
- async fn render(data: &JVStatusOutput) -> Result<JVRenderResult, CmdRenderError> {
- let mut r = JVRenderResult::default();
+#[result_renderer(JVStatusRenderer)]
+pub async fn render(data: &JVStatusOutput) -> Result<JVRenderResult, CmdRenderError> {
+ 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<Argument, Input, Collect, Output, Renderer>
+pub trait JVCommand<Argument, Input, Collect>
where
Argument: clap::Parser + Send,
Input: Send,
- Output: Serialize + Send + Sync,
Collect: Send,
- Renderer: JVResultRenderer<Output> + 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<String>,
ctx: JVCommandContext,
- renderer: String,
- ) -> impl Future<Output = Result<JVRenderResult, CmdProcessError>> + Send
- where
- Self: Sync,
- {
+ renderer_override: String,
+ ) -> impl Future<Output = Result<JVRenderResult, CmdProcessError>> + 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<String>,
ctx: JVCommandContext,
- ) -> impl Future<Output = Result<JVRenderResult, CmdProcessError>> + Send
- where
- Self: Sync,
- {
- Self::process_with_renderer::<Renderer>(args, ctx)
+ ) -> impl Future<Output = Result<JVRenderResult, CmdProcessError>> + 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<R: JVResultRenderer<Output> + Send>(
+ fn process(
args: Vec<String>,
ctx: JVCommandContext,
- ) -> impl Future<Output = Result<JVRenderResult, CmdProcessError>> + Send
- where
- Self: Sync,
+ ) -> impl Future<Output = Result<(Box<dyn Any + Send + 'static>, 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<Output = Result<Output, CmdExecuteError>> + Send;
+ ) -> impl Future<Output = Result<(Box<dyn Any + Send + 'static>, String), CmdExecuteError>> + Send;
+
+ /// Get output type mapping
+ fn get_output_type_mapping() -> HashMap<String, TypeId>;
}
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 <args...>`
+/// /// get_data.rs matches JVGetDataCommand, invoked using `jv get data <args...>`
+/// 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<In, CmdPrepareError> {
+/// todo!()
+/// }
+///
+/// /// Collect necessary local information for execution
+/// async fn collect(args: &Arg, ctx: &JVCommandContext) -> Result<Collect, CmdPrepareError> {
+/// let reader = LocalWorkspaceReader::default();
+/// todo!()
+/// }
+///
+/// /// Execution phase, call core layer or other custom logic
+/// #[exec]
+/// async fn exec(
+/// input: In,
+/// collect: Collect,
+/// ) -> Result<(Box<dyn std::any::Any + Send + 'static>, 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<In, CmdPrepareError> {
+/// todo!()
+/// }
+///
+/// async fn collect(args: &Arg, ctx: &JVCommandContext) -> Result<Collect, CmdPrepareError> {
+/// let reader = LocalWorkspaceReader::default();
+/// todo!()
+/// }
+///
+/// #[exec]
+/// async fn exec(
+/// input: In,
+/// collect: Collect,
+/// ) -> Result<(Box<dyn std::any::Any + Send + 'static>, String), CmdExecuteError> {
+/// todo!();
+/// cmd_output!(output, JVCustomOutput)
+/// }
+/// ```
+macro_rules! command_template {
+ () => {
+ impl $crate::systems::cmd::cmd_system::JVCommand<Arg, In, Collect> for Cmd {
+ fn get_help_str() -> String {
+ help_str()
+ }
+
+ async fn prepare(
+ args: &Arg,
+ ctx: &$crate::systems::cmd::cmd_system::JVCommandContext,
+ ) -> Result<In, $crate::systems::cmd::errors::CmdPrepareError> {
+ prepare(args, ctx).await
+ }
+
+ async fn collect(
+ args: &Arg,
+ ctx: &$crate::systems::cmd::cmd_system::JVCommandContext,
+ ) -> Result<Collect, $crate::systems::cmd::errors::CmdPrepareError> {
+ collect(args, ctx).await
+ }
+
+ async fn exec(
+ input: In,
+ collect: Collect,
+ ) -> Result<
+ (Box<dyn std::any::Any + Send + 'static>, String),
+ $crate::systems::cmd::errors::CmdExecuteError,
+ > {
+ exec(input, collect).await
+ }
+
+ fn get_output_type_mapping() -> std::collections::HashMap<String, std::any::TypeId> {
+ 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<String>,
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<dyn Any + Send + 'static>,
+ type_name: String,
+) -> Result<JVRenderResult, CmdRenderError> {
+ let type_name_str = type_name.as_str();
+ include!("_specific_renderer_matching.rs")
+}
diff --git a/src/systems/cmd/renderer.rs b/src/systems/render/renderer.rs
index 1849ee9..9060683 100644
--- a/src/systems/cmd/renderer.rs
+++ b/src/systems/render/renderer.rs
@@ -1,16 +1,15 @@
use std::fmt::{Display, Formatter};
-
-use serde::Serialize;
+use std::future::Future;
use crate::systems::cmd::errors::CmdRenderError;
-pub trait JVResultRenderer<Data>
-where
- Data: Serialize,
-{
+pub trait JVResultRenderer<Data> {
fn render(
data: &Data,
) -> impl Future<Output = Result<JVRenderResult, CmdRenderError>> + Send + Sync;
+
+ fn get_type_id(&self) -> std::any::TypeId;
+ fn get_data_type_id(&self) -> std::any::TypeId;
}
#[derive(Default, Debug, PartialEq)]
diff --git a/templates/_registry.rs.template b/templates/_commands.rs.template
index 957484c..84d2db4 100644
--- a/templates/_registry.rs.template
+++ b/templates/_commands.rs.template
@@ -8,18 +8,25 @@ pub async fn jv_cmd_process_node(
args: Vec<String>,
ctx: JVCommandContext,
renderer_override: String
-) -> Result<crate::systems::cmd::renderer::JVRenderResult, crate::systems::cmd::errors::CmdProcessError> {
+) -> Result<crate::systems::render::renderer::JVRenderResult, crate::systems::cmd::errors::CmdProcessError> {
match node {
// PROCESS
// -- TEMPLATE START --
// Command `<<KEY>>`
"<<NODE_NAME>>" => {
- return crate::<<COMMAND_TYPE>>::process_with_renderer_flag(
- args,
- ctx,
- renderer_override
- )
- .await;
+ if renderer_override == "default" {
+ return crate::<<COMMAND_TYPE>>::process_to_render_system(
+ args, ctx,
+ )
+ .await;
+ } else {
+ return crate::<<COMMAND_TYPE>>::process_to_renderer_override(
+ args,
+ ctx,
+ renderer_override,
+ )
+ .await;
+ }
}
// -- TEMPLATE END --
_ => {}
diff --git a/templates/_renderers.rs.template b/templates/_override_renderer_dispatcher.rs.template
index 37f0f1b..64d2f40 100644
--- a/templates/_renderers.rs.template
+++ b/templates/_override_renderer_dispatcher.rs.template
@@ -1,16 +1,13 @@
-match renderer_str {
+match renderer_override {
// MATCH
// -- TEMPLATE START --
"<<NAME>>" => {
- Self::process_with_renderer::<
- RendererType
- >(args, ctx)
- .await
+ RendererType::render(&concrete_data).await
}
// -- TEMPLATE END --
_ => {
return Err(CmdProcessError::Render(CmdRenderError::RendererNotFound(
- renderer_str.to_string(),
+ 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::<JVOutputType>()
+ .map_err(|_| CmdProcessError::DowncastFailed)?;
+ include!("../render/_override_renderer_dispatcher.rs")
+ }
+ _ => return Err(CmdProcessError::NoMatchingCommand),
+// -- TEMPLATE END --
+}
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::<OutputType>()
+ .unwrap(),
+ )
+ .await
+ }
+// -- TEMPLATE END --
+ _ => Err(CmdRenderError::RendererNotFound(type_name)),
+}