From aca8b408755f9041da9ee083c625de2a8d8c6785 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 22 Jan 2026 08:32:29 +0800 Subject: Refactor CLI command processing with new architecture --- src/bin/jvn.rs | 119 ++++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 3 ++ src/subcmd.rs | 5 ++ src/subcmd/cmd.rs | 85 +++++++++++++++++++++++++++++++ src/subcmd/cmds.rs | 2 + src/subcmd/cmds/status.rs | 45 +++++++++++++++++ src/subcmd/cmds/template.rs | 45 +++++++++++++++++ src/subcmd/errors.rs | 107 +++++++++++++++++++++++++++++++++++++++ src/subcmd/processer.rs | 40 +++++++++++++++ src/subcmd/renderer.rs | 54 ++++++++++++++++++++ 10 files changed, 495 insertions(+), 10 deletions(-) create mode 100644 src/subcmd.rs create mode 100644 src/subcmd/cmd.rs create mode 100644 src/subcmd/cmds.rs create mode 100644 src/subcmd/cmds/status.rs create mode 100644 src/subcmd/cmds/template.rs create mode 100644 src/subcmd/errors.rs create mode 100644 src/subcmd/processer.rs create mode 100644 src/subcmd/renderer.rs (limited to 'src') diff --git a/src/bin/jvn.rs b/src/bin/jvn.rs index cc93fb1..86e3421 100644 --- a/src/bin/jvn.rs +++ b/src/bin/jvn.rs @@ -1,24 +1,123 @@ -use just_enough_vcs_cli::{subcmd::cmds::_processer::jv_cmd_process, utils::env::current_locales}; -use rust_i18n::set_locale; +use just_enough_vcs_cli::subcmd::cmd::JVCommandContext; +use just_enough_vcs_cli::utils::display::md; +use just_enough_vcs_cli::{ + subcmd::{errors::CmdProcessError, processer::jv_cmd_process}, + utils::env::current_locales, +}; +use rust_i18n::{set_locale, t}; -rust_i18n::i18n!("resources/locales/jv", fallback = "en"); +rust_i18n::i18n!("resources/locales/jvn", fallback = "en"); + +macro_rules! special_flag { + ($args:expr, $flag:expr) => {{ + let flag = $flag; + let found = $args.iter().any(|arg| arg == flag); + $args.retain(|arg| arg != flag); + found + }}; +} + +macro_rules! special_argument { + ($args:expr, $flag:expr) => {{ + let flag = $flag; + let mut value: Option = None; + let mut i = 0; + while i < $args.len() { + if $args[i] == flag { + if i + 1 < $args.len() { + value = Some($args[i + 1].clone()); + $args.remove(i + 1); + $args.remove(i); + } else { + value = None; + $args.remove(i); + } + break; + } + i += 1; + } + value + }}; +} #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() { + // Collect arguments + let mut args: Vec = std::env::args().skip(1).collect(); + // Init i18n - set_locale(¤t_locales()); + let lang = special_argument!(args, "--lang").unwrap_or(current_locales()); + set_locale(&lang); // Init colored #[cfg(windows)] colored::control::set_virtual_terminal(true).unwrap(); - // Collect arguments - let args: Vec = std::env::args().collect(); + let no_error_logs = special_flag!(args, "--no-error-logs"); + let quiet = special_flag!(args, "--quiet") || special_flag!(args, "-q"); + let help = special_flag!(args, "--help") || special_flag!(args, "-h"); + let confirmed = special_flag!(args, "--confirm") || special_flag!(args, "-C"); // Process commands - let render_result = jv_cmd_process(args).await.unwrap_or_default(); + let render_result = match jv_cmd_process(args, JVCommandContext { help, confirmed }).await { + Ok(result) => result, + Err(e) => { + if !no_error_logs { + match e { + CmdProcessError::Prepare(cmd_prepare_error) => { + eprintln!( + "{}", + md(t!("process_error.prepare_error", error = cmd_prepare_error)) + ); + } + CmdProcessError::Execute(cmd_execute_error) => { + eprintln!( + "{}", + md(t!("process_error.execute_error", error = cmd_execute_error)) + ); + } + CmdProcessError::Render(cmd_render_error) => { + eprintln!( + "{}", + md(t!("process_error.render_error", error = cmd_render_error)) + ); + } + CmdProcessError::Error(error) => { + eprintln!("{}", md(t!("process_error.other", error = error))); + } + CmdProcessError::NoNodeFound(node) => { + eprintln!("{}", md(t!("process_error.no_node_found", node = node))); + } + CmdProcessError::NoMatchingCommand => { + eprintln!("{}", md(t!("process_error.no_matching_command"))); + } + CmdProcessError::AmbiguousCommand(nodes) => { + let nodes_list = nodes + .iter() + .enumerate() + .map(|(i, node)| format!("{}. {}", i + 1, node)) + .collect::>() + .join("\n"); + eprintln!( + "{}", + md(t!("process_error.ambiguous_command", nodes = nodes_list)) + ); + } + CmdProcessError::ParseError(help) => { + if help.trim().len() < 1 { + eprintln!("{}", md(t!("process_error.parse_error"))); + } else { + eprintln!("{}", help) + } + } + } + } + std::process::exit(1); + } + }; // Print - print!("{}", render_result); - Ok(()) + if !quiet { + print!("{}", render_result); + } } diff --git a/src/lib.rs b/src/lib.rs index a0e7a53..2a06e0d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,3 +6,6 @@ pub mod data; /// Json Format pub mod output; + +/// Command +pub mod subcmd; diff --git a/src/subcmd.rs b/src/subcmd.rs new file mode 100644 index 0000000..8c2f733 --- /dev/null +++ b/src/subcmd.rs @@ -0,0 +1,5 @@ +pub mod cmd; +pub mod cmds; +pub mod errors; +pub mod processer; +pub mod renderer; diff --git a/src/subcmd/cmd.rs b/src/subcmd/cmd.rs new file mode 100644 index 0000000..10ad893 --- /dev/null +++ b/src/subcmd/cmd.rs @@ -0,0 +1,85 @@ +use serde::Serialize; + +use crate::{ + r_println, + subcmd::{ + errors::{CmdExecuteError, CmdPrepareError, CmdProcessError}, + renderer::{JVRenderResult, JVResultRenderer}, + }, +}; +use std::future::Future; + +pub struct JVCommandContext { + pub help: bool, + pub confirmed: bool, +} + +pub trait JVCommand +where + Argument: clap::Parser + Send + Sync, + Input: Send + Sync, + Output: Serialize + Send + Sync, + Renderer: JVResultRenderer + Send + Sync, +{ + /// Get help string for the command + fn get_help_str() -> String; + + /// performing any necessary post-execution processing + fn process( + args: Vec, + ctx: JVCommandContext, + ) -> impl Future> + Send + Sync + where + Self: Sync, + { + Self::process_with_renderer::(args, ctx) + } + + /// Process the command output with a custom renderer, + /// performing any necessary post-execution processing + fn process_with_renderer + Send + Sync>( + args: Vec, + ctx: JVCommandContext, + ) -> impl Future> + Send + Sync + where + Self: Sync, + { + async move { + let mut full_args = vec!["jv".to_string()]; + full_args.extend(args); + let parsed_args = match Argument::try_parse_from(full_args) { + Ok(args) => args, + 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 = match Self::prepare(parsed_args, ctx).await { + Ok(input) => input, + Err(e) => return Err(CmdProcessError::from(e)), + }; + let output = match Self::exec(input).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)), + } + } + } + + /// Prepare to run the command, + /// converting Clap input into the command's supported input + fn prepare( + args: Argument, + ctx: JVCommandContext, + ) -> impl Future> + Send + Sync; + + /// Run the command phase, + /// returning an output structure, waiting for rendering + fn exec(args: Input) -> impl Future> + Send + Sync; +} diff --git a/src/subcmd/cmds.rs b/src/subcmd/cmds.rs new file mode 100644 index 0000000..e06480c --- /dev/null +++ b/src/subcmd/cmds.rs @@ -0,0 +1,2 @@ +pub mod _registry; +pub mod status; diff --git a/src/subcmd/cmds/status.rs b/src/subcmd/cmds/status.rs new file mode 100644 index 0000000..2b92df6 --- /dev/null +++ b/src/subcmd/cmds/status.rs @@ -0,0 +1,45 @@ +use clap::Parser; +use serde::Serialize; + +use crate::subcmd::{ + cmd::{JVCommand, JVCommandContext}, + errors::{CmdExecuteError, CmdPrepareError, CmdRenderError}, + renderer::{JVRenderResult, JVResultRenderer}, +}; + +pub struct JVStatusCommand; + +#[derive(Parser, Debug)] +pub struct JVStatusArgument; + +pub struct JVStatusInput; + +#[derive(Serialize)] +pub struct JVStatusOutput; + +impl JVCommand + for JVStatusCommand +{ + async fn prepare( + args: JVStatusArgument, + ctx: JVCommandContext, + ) -> Result { + todo!() + } + + async fn exec(args: JVStatusInput) -> Result { + todo!() + } + + fn get_help_str() -> String { + "".to_string() + } +} + +pub struct JVStatusRenderer; + +impl JVResultRenderer for JVStatusRenderer { + async fn render(data: &JVStatusOutput) -> Result { + todo!() + } +} diff --git a/src/subcmd/cmds/template.rs b/src/subcmd/cmds/template.rs new file mode 100644 index 0000000..8874121 --- /dev/null +++ b/src/subcmd/cmds/template.rs @@ -0,0 +1,45 @@ +use clap::Parser; +use serde::Serialize; + +use crate::subcmd::{ + cmd::JVCommand, + errors::{CmdExecuteError, CmdPrepareError, CmdRenderError}, + renderer::{JVRenderResult, JVResultRenderer}, +}; + +pub struct JVUnknownCommand; + +#[derive(Parser, Debug)] +pub struct JVUnknownArgument; + +pub struct JVUnknownInput; + +#[derive(Serialize)] +pub struct JVUnknownOutput; + +impl JVCommand + for JVUnknownCommand +{ + async fn prepare( + _args: JVUnknownArgument, + _ctx: JVCommandContext, + ) -> Result { + todo!() + } + + async fn exec(_args: JVUnknownInput) -> Result { + todo!() + } + + fn get_help_str() -> String { + "".to_string() + } +} + +pub struct JVStatusRenderer; + +impl JVResultRenderer for JVStatusRenderer { + async fn render(_data: &JVUnknownOutput) -> Result { + todo!() + } +} diff --git a/src/subcmd/errors.rs b/src/subcmd/errors.rs new file mode 100644 index 0000000..e1cf835 --- /dev/null +++ b/src/subcmd/errors.rs @@ -0,0 +1,107 @@ +#[derive(thiserror::Error, Debug)] +pub enum CmdPrepareError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("{0}")] + Error(String), +} + +impl CmdPrepareError { + pub fn new(msg: impl AsRef) -> Self { + CmdPrepareError::Error(msg.as_ref().to_string()) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum CmdExecuteError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Content not prepared, cannot run")] + Prepare(#[from] CmdPrepareError), + + #[error("{0}")] + Error(String), +} + +impl CmdExecuteError { + pub fn new(msg: impl AsRef) -> Self { + CmdExecuteError::Error(msg.as_ref().to_string()) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum CmdRenderError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Preparation failed, cannot render")] + Prepare(#[from] CmdPrepareError), + + #[error("Execution failed, no output content obtained before rendering")] + Execute(#[from] CmdExecuteError), + + #[error("{0}")] + Error(String), +} + +impl CmdRenderError { + pub fn new(msg: impl AsRef) -> Self { + CmdRenderError::Error(msg.as_ref().to_string()) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum CmdProcessError { + #[error("Prepare error: {0}")] + Prepare(#[from] CmdPrepareError), + + #[error("Execute error: {0}")] + Execute(#[from] CmdExecuteError), + + #[error("Render error: {0}")] + Render(#[from] CmdRenderError), + + #[error("{0}")] + Error(String), + + #[error("Node `{0}` not found!")] + NoNodeFound(String), + + #[error("No matching command found")] + NoMatchingCommand, + + #[error("Ambiguous command, multiple matches found")] + AmbiguousCommand(Vec), + + #[error("Parse error")] + ParseError(String), +} + +impl CmdProcessError { + pub fn new(msg: impl AsRef) -> Self { + CmdProcessError::Error(msg.as_ref().to_string()) + } + + pub fn prepare_err(&self) -> Option<&CmdPrepareError> { + match self { + CmdProcessError::Prepare(e) => Some(e), + _ => None, + } + } + + pub fn execute_err(&self) -> Option<&CmdExecuteError> { + match self { + CmdProcessError::Execute(e) => Some(e), + _ => None, + } + } + + pub fn render_err(&self) -> Option<&CmdRenderError> { + match self { + CmdProcessError::Render(e) => Some(e), + _ => None, + } + } +} diff --git a/src/subcmd/processer.rs b/src/subcmd/processer.rs new file mode 100644 index 0000000..5849c62 --- /dev/null +++ b/src/subcmd/processer.rs @@ -0,0 +1,40 @@ +use crate::subcmd::cmd::JVCommandContext; +use crate::subcmd::cmds::_registry::{jv_cmd_nodes, jv_cmd_process_node}; +use crate::subcmd::errors::CmdProcessError; +use crate::subcmd::renderer::JVRenderResult; + +pub async fn jv_cmd_process( + args: Vec, + ctx: JVCommandContext, +) -> Result { + let nodes = jv_cmd_nodes(); + let command = args.join(" "); + + // Find nodes that match the beginning of the command + let matching_nodes: Vec<&String> = nodes + .iter() + .filter(|node| command.starts_with(node.as_str())) + .collect(); + + match matching_nodes.len() { + 0 => { + // No matching node found + return Err(CmdProcessError::NoMatchingCommand); + } + 1 => { + let matched_prefix = matching_nodes[0]; + let prefix_len = matched_prefix.split_whitespace().count(); + let trimmed_args: Vec = args.into_iter().skip(prefix_len).collect(); + return jv_cmd_process_node(matched_prefix, trimmed_args, ctx).await; + } + _ => { + // Multiple matching nodes found + return Err(CmdProcessError::AmbiguousCommand( + matching_nodes + .iter() + .map(|s| s.to_string()) + .collect::>(), + )); + } + } +} diff --git a/src/subcmd/renderer.rs b/src/subcmd/renderer.rs new file mode 100644 index 0000000..99a67f1 --- /dev/null +++ b/src/subcmd/renderer.rs @@ -0,0 +1,54 @@ +use std::fmt::{Display, Formatter}; + +use serde::Serialize; + +use crate::subcmd::errors::CmdRenderError; + +pub trait JVResultRenderer +where + Data: Serialize, +{ + fn render( + data: &Data, + ) -> impl Future> + Send + Sync; +} + +#[derive(Default, Debug, PartialEq)] +pub struct JVRenderResult { + render_text: String, +} + +impl Display for JVRenderResult { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}\n", self.render_text.trim()) + } +} + +impl JVRenderResult { + pub fn print(&mut self, text: &str) { + self.render_text.push_str(text); + } + + pub fn println(&mut self, text: &str) { + self.render_text.push_str(text); + self.render_text.push('\n'); + } + + pub fn clear(&mut self) { + self.render_text.clear(); + } +} + +#[macro_export] +macro_rules! r_print { + ($result:expr, $($arg:tt)*) => { + $result.print(&format!($($arg)*)); + }; +} + +#[macro_export] +macro_rules! r_println { + ($result:expr, $($arg:tt)*) => { + $result.println(&format!($($arg)*)); + }; +} -- cgit