From 626536ce51975b051fea087620bc1eb7f6bc69d3 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 23 Jan 2026 04:49:34 +0800 Subject: Reorganize project structure into systems and assets --- src/arguments.rs | 0 src/arguments/status.rs | 0 src/bin/jvn.rs | 4 +- src/cmd.rs | 6 -- src/cmd/cmd_system.rs | 100 -------------------------- src/cmd/cmds.rs | 2 - src/cmd/cmds/status.rs | 122 -------------------------------- src/cmd/cmds/template.rs | 45 ------------ src/cmd/errors.rs | 139 ------------------------------------- src/cmd/processer.rs | 41 ----------- src/cmd/renderer.rs | 54 -------------- src/cmd/renderers.rs | 1 - src/cmd/renderers/json_renderer.rs | 44 ------------ src/cmds.rs | 1 + src/cmds/status.rs | 122 ++++++++++++++++++++++++++++++++ src/inputs.rs | 0 src/lib.rs | 19 ++++- src/outputs.rs | 1 + src/renderers.rs | 2 + src/renderers/json.rs | 27 +++++++ src/renderers/json_pretty.rs | 26 +++++++ src/systems.rs | 1 + src/systems/cmd.rs | 5 ++ src/systems/cmd/cmd_system.rs | 100 ++++++++++++++++++++++++++ src/systems/cmd/errors.rs | 139 +++++++++++++++++++++++++++++++++++++ src/systems/cmd/processer.rs | 41 +++++++++++ src/systems/cmd/renderer.rs | 54 ++++++++++++++ src/utils/workspace_reader.rs | 2 +- 28 files changed, 539 insertions(+), 559 deletions(-) create mode 100644 src/arguments.rs create mode 100644 src/arguments/status.rs delete mode 100644 src/cmd.rs delete mode 100644 src/cmd/cmd_system.rs delete mode 100644 src/cmd/cmds.rs delete mode 100644 src/cmd/cmds/status.rs delete mode 100644 src/cmd/cmds/template.rs delete mode 100644 src/cmd/errors.rs delete mode 100644 src/cmd/processer.rs delete mode 100644 src/cmd/renderer.rs delete mode 100644 src/cmd/renderers.rs delete mode 100644 src/cmd/renderers/json_renderer.rs create mode 100644 src/cmds.rs create mode 100644 src/cmds/status.rs create mode 100644 src/inputs.rs create mode 100644 src/outputs.rs create mode 100644 src/renderers.rs create mode 100644 src/renderers/json.rs create mode 100644 src/renderers/json_pretty.rs create mode 100644 src/systems.rs create mode 100644 src/systems/cmd.rs create mode 100644 src/systems/cmd/cmd_system.rs create mode 100644 src/systems/cmd/errors.rs create mode 100644 src/systems/cmd/processer.rs create mode 100644 src/systems/cmd/renderer.rs (limited to 'src') diff --git a/src/arguments.rs b/src/arguments.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/arguments/status.rs b/src/arguments/status.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/bin/jvn.rs b/src/bin/jvn.rs index 9943ab4..25bf219 100644 --- a/src/bin/jvn.rs +++ b/src/bin/jvn.rs @@ -1,7 +1,7 @@ -use just_enough_vcs_cli::cmd::cmd_system::JVCommandContext; +use just_enough_vcs_cli::systems::cmd::cmd_system::JVCommandContext; use just_enough_vcs_cli::utils::display::md; use just_enough_vcs_cli::{ - cmd::{errors::CmdProcessError, processer::jv_cmd_process}, + systems::cmd::{errors::CmdProcessError, processer::jv_cmd_process}, utils::env::current_locales, }; use rust_i18n::{set_locale, t}; diff --git a/src/cmd.rs b/src/cmd.rs deleted file mode 100644 index 2c9cac6..0000000 --- a/src/cmd.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod cmd_system; -pub mod cmds; -pub mod errors; -pub mod processer; -pub mod renderer; -pub mod renderers; diff --git a/src/cmd/cmd_system.rs b/src/cmd/cmd_system.rs deleted file mode 100644 index 09daf5a..0000000 --- a/src/cmd/cmd_system.rs +++ /dev/null @@ -1,100 +0,0 @@ -use serde::Serialize; - -use crate::{ - cmd::{ - errors::{CmdExecuteError, CmdPrepareError, CmdProcessError, CmdRenderError}, - renderer::{JVRenderResult, JVResultRenderer}, - }, - r_println, -}; -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; - - /// Process the command with a specified renderer, performing any necessary post-execution processing - fn process_with_renderer_flag( - args: Vec, - ctx: JVCommandContext, - renderer: String, - ) -> impl Future> + Send - where - Self: Sync, - { - async move { - let renderer_str = renderer.as_str(); - include!("renderers/_renderers.rs") - } - } - - /// performing any necessary post-execution processing - fn process( - args: Vec, - ctx: JVCommandContext, - ) -> impl Future> + Send - 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 - 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; - - /// Run the command phase, - /// returning an output structure, waiting for rendering - fn exec(input: Input) -> impl Future> + Send; -} diff --git a/src/cmd/cmds.rs b/src/cmd/cmds.rs deleted file mode 100644 index e06480c..0000000 --- a/src/cmd/cmds.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod _registry; -pub mod status; diff --git a/src/cmd/cmds/status.rs b/src/cmd/cmds/status.rs deleted file mode 100644 index d73a28a..0000000 --- a/src/cmd/cmds/status.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - path::PathBuf, - time::SystemTime, -}; - -use clap::Parser; -use just_enough_vcs::vcs::data::{ - local::workspace_analyzer::{ - CreatedRelativePathBuf, FromRelativePathBuf, LostRelativePathBuf, ModifiedRelativePathBuf, - ToRelativePathBuf, - }, - member::MemberId, - sheet::SheetName, - vault::virtual_file::VirtualFileId, -}; -use serde::Serialize; - -use crate::{ - cmd::{ - cmd_system::{JVCommand, JVCommandContext}, - errors::{CmdExecuteError, CmdPrepareError, CmdRenderError}, - renderer::{JVRenderResult, JVResultRenderer}, - }, - utils::workspace_reader::LocalWorkspaceReader, -}; - -pub struct JVStatusCommand; - -#[derive(Parser, Debug)] -pub struct JVStatusArgument; - -#[derive(Serialize)] -pub struct JVStatusResult { - pub current_account: MemberId, - pub current_sheet: SheetName, - pub moved: HashMap, - pub created: HashSet, - pub lost: HashSet, - pub erased: HashSet, - pub modified: HashSet, - pub update_time: SystemTime, - pub now_time: SystemTime, -} - -impl Default for JVStatusResult { - fn default() -> Self { - Self { - current_account: MemberId::default(), - current_sheet: SheetName::default(), - moved: HashMap::default(), - created: HashSet::default(), - lost: HashSet::default(), - erased: HashSet::default(), - modified: HashSet::default(), - update_time: SystemTime::now(), - now_time: SystemTime::now(), - } - } -} - -impl JVCommand - for JVStatusCommand -{ - async fn prepare( - _args: JVStatusArgument, - _ctx: JVCommandContext, - ) -> Result { - // Initialize a reader for the local workspace and a default result structure - let mut reader = LocalWorkspaceReader::default(); - let mut input = JVStatusResult::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?; - - // 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 - input.current_account = account; - input.current_sheet = sheet_name; - input.moved = analyzed.moved; - input.created = analyzed.created; - input.lost = analyzed.lost; - input.erased = analyzed.erased; - input.modified = analyzed.modified; - input.update_time = update_time; - input.now_time = now_time; - - Ok(input) - } - - async fn exec(input: JVStatusResult) -> Result { - Ok(input) // Analyze command, no needs execute - } - - fn get_help_str() -> String { - "".to_string() - } -} - -pub struct JVStatusRenderer; - -impl JVResultRenderer for JVStatusRenderer { - async fn render(data: &JVStatusResult) -> Result { - todo!() - } -} diff --git a/src/cmd/cmds/template.rs b/src/cmd/cmds/template.rs deleted file mode 100644 index 1c56c29..0000000 --- a/src/cmd/cmds/template.rs +++ /dev/null @@ -1,45 +0,0 @@ -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(_input: 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/cmd/errors.rs b/src/cmd/errors.rs deleted file mode 100644 index 358d15a..0000000 --- a/src/cmd/errors.rs +++ /dev/null @@ -1,139 +0,0 @@ -use just_enough_vcs::vcs::data::{member::MemberId, sheet::SheetName}; - -#[derive(thiserror::Error, Debug)] -pub enum CmdPrepareError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("{0}")] - Error(String), - - #[error("LocalWorkspace not found")] - LocalWorkspaceNotFound, - - #[error("LocalConfig not found")] - LocalConfigNotFound, - - #[error("LatestInfo not found")] - LatestInfoNotFound, - - #[error("LatestFileData of {0} not found")] - LatestFileDataNotExist(MemberId), - - #[error("CachedSheet `{0}` not found")] - CachedSheetNotFound(SheetName), - - #[error("LocalSheet `{0}/{1}` not found")] - LocalSheetNotFound(MemberId, SheetName), - - #[error("LocalStatusAnalyzeFailed")] - LocalStatusAnalyzeFailed, - - #[error("No sheet in use")] - NoSheetInUse, -} - -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), - - #[error("Serialize failed, {0}")] - SerializeFailed(String), - - #[error("Renderer `{0}` not found")] - RendererNotFound(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/cmd/processer.rs b/src/cmd/processer.rs deleted file mode 100644 index f5fc2a6..0000000 --- a/src/cmd/processer.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::cmd::cmd_system::JVCommandContext; -use crate::cmd::cmds::_registry::{jv_cmd_nodes, jv_cmd_process_node}; -use crate::cmd::errors::CmdProcessError; -use crate::cmd::renderer::JVRenderResult; - -pub async fn jv_cmd_process( - args: Vec, - ctx: JVCommandContext, - renderer_override: String, -) -> 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, renderer_override).await; - } - _ => { - // Multiple matching nodes found - return Err(CmdProcessError::AmbiguousCommand( - matching_nodes - .iter() - .map(|s| s.to_string()) - .collect::>(), - )); - } - } -} diff --git a/src/cmd/renderer.rs b/src/cmd/renderer.rs deleted file mode 100644 index eefa0f6..0000000 --- a/src/cmd/renderer.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use serde::Serialize; - -use crate::cmd::errors::CmdRenderError; - -pub trait JVResultRenderer -where - Data: Serialize, -{ - fn render( - data: &Data, - ) -> impl Future> + Send + Sync; -} - -#[derive(Default, Debug, PartialEq)] -pub struct JVRenderResult { - render_text: String, -} - -impl Display for JVRenderResult { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}\n", self.render_text.trim()) - } -} - -impl JVRenderResult { - pub fn print(&mut self, text: &str) { - self.render_text.push_str(text); - } - - pub fn println(&mut self, text: &str) { - self.render_text.push_str(text); - self.render_text.push('\n'); - } - - pub fn clear(&mut self) { - self.render_text.clear(); - } -} - -#[macro_export] -macro_rules! r_print { - ($result:expr, $($arg:tt)*) => { - $result.print(&format!($($arg)*)); - }; -} - -#[macro_export] -macro_rules! r_println { - ($result:expr, $($arg:tt)*) => { - $result.println(&format!($($arg)*)); - }; -} diff --git a/src/cmd/renderers.rs b/src/cmd/renderers.rs deleted file mode 100644 index 91fa88e..0000000 --- a/src/cmd/renderers.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod json_renderer; diff --git a/src/cmd/renderers/json_renderer.rs b/src/cmd/renderers/json_renderer.rs deleted file mode 100644 index 14c1f81..0000000 --- a/src/cmd/renderers/json_renderer.rs +++ /dev/null @@ -1,44 +0,0 @@ -use serde::Serialize; -use serde_json; - -use crate::{ - cmd::{ - errors::CmdRenderError, - renderer::{JVRenderResult, JVResultRenderer}, - }, - r_print, -}; - -pub struct JVResultJsonRenderer; - -impl JVResultRenderer for JVResultJsonRenderer -where - T: Serialize + Sync, -{ - async fn render(data: &T) -> Result { - let mut r = JVRenderResult::default(); - let json_string = serde_json::to_string(data) - .map_err(|e| CmdRenderError::SerializeFailed(e.to_string()))?; - - r_print!(r, "{}", json_string); - - Ok(r) - } -} - -pub struct JVResultPrettyJsonRenderer; - -impl JVResultRenderer for JVResultPrettyJsonRenderer -where - T: Serialize + Sync, -{ - async fn render(data: &T) -> Result { - let mut r = JVRenderResult::default(); - let json_string = serde_json::to_string_pretty(data) - .map_err(|e| CmdRenderError::SerializeFailed(e.to_string()))?; - - r_print!(r, "{}", json_string); - - Ok(r) - } -} diff --git a/src/cmds.rs b/src/cmds.rs new file mode 100644 index 0000000..822c729 --- /dev/null +++ b/src/cmds.rs @@ -0,0 +1 @@ +pub mod status; diff --git a/src/cmds/status.rs b/src/cmds/status.rs new file mode 100644 index 0000000..57e9cde --- /dev/null +++ b/src/cmds/status.rs @@ -0,0 +1,122 @@ +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, + time::SystemTime, +}; + +use clap::Parser; +use just_enough_vcs::vcs::data::{ + local::workspace_analyzer::{ + CreatedRelativePathBuf, FromRelativePathBuf, LostRelativePathBuf, ModifiedRelativePathBuf, + ToRelativePathBuf, + }, + member::MemberId, + sheet::SheetName, + vault::virtual_file::VirtualFileId, +}; +use serde::Serialize; + +use crate::{ + systems::cmd::{ + cmd_system::{JVCommand, JVCommandContext}, + errors::{CmdExecuteError, CmdPrepareError, CmdRenderError}, + renderer::{JVRenderResult, JVResultRenderer}, + }, + utils::workspace_reader::LocalWorkspaceReader, +}; + +pub struct JVStatusCommand; + +#[derive(Parser, Debug)] +pub struct JVStatusArgument; + +#[derive(Serialize)] +pub struct JVStatusResult { + pub current_account: MemberId, + pub current_sheet: SheetName, + pub moved: HashMap, + pub created: HashSet, + pub lost: HashSet, + pub erased: HashSet, + pub modified: HashSet, + pub update_time: SystemTime, + pub now_time: SystemTime, +} + +impl Default for JVStatusResult { + fn default() -> Self { + Self { + current_account: MemberId::default(), + current_sheet: SheetName::default(), + moved: HashMap::default(), + created: HashSet::default(), + lost: HashSet::default(), + erased: HashSet::default(), + modified: HashSet::default(), + update_time: SystemTime::now(), + now_time: SystemTime::now(), + } + } +} + +impl JVCommand + for JVStatusCommand +{ + async fn prepare( + _args: JVStatusArgument, + _ctx: JVCommandContext, + ) -> Result { + // Initialize a reader for the local workspace and a default result structure + let mut reader = LocalWorkspaceReader::default(); + let mut input = JVStatusResult::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?; + + // 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 + input.current_account = account; + input.current_sheet = sheet_name; + input.moved = analyzed.moved; + input.created = analyzed.created; + input.lost = analyzed.lost; + input.erased = analyzed.erased; + input.modified = analyzed.modified; + input.update_time = update_time; + input.now_time = now_time; + + Ok(input) + } + + async fn exec(input: JVStatusResult) -> Result { + Ok(input) // Analyze command, no needs execute + } + + fn get_help_str() -> String { + "".to_string() + } +} + +pub struct JVStatusRenderer; + +impl JVResultRenderer for JVStatusRenderer { + async fn render(data: &JVStatusResult) -> Result { + todo!() + } +} diff --git a/src/inputs.rs b/src/inputs.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs index 7c9dd8f..bc86288 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +// --- LIBS --- + /// Utils pub mod utils; @@ -7,5 +9,18 @@ pub mod data; /// Json Format pub mod leegacy_json_output; -/// Command -pub mod cmd; +/// Systems +pub mod systems; + +// --- ASSETS --- + +/// Commands +pub mod cmds; + +/// Command Data +pub mod arguments; +pub mod inputs; +pub mod outputs; + +/// Result Renderers +pub mod renderers; diff --git a/src/outputs.rs b/src/outputs.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/outputs.rs @@ -0,0 +1 @@ + diff --git a/src/renderers.rs b/src/renderers.rs new file mode 100644 index 0000000..60ad190 --- /dev/null +++ b/src/renderers.rs @@ -0,0 +1,2 @@ +pub mod json; +pub mod json_pretty; diff --git a/src/renderers/json.rs b/src/renderers/json.rs new file mode 100644 index 0000000..9a3105d --- /dev/null +++ b/src/renderers/json.rs @@ -0,0 +1,27 @@ +use serde::Serialize; +use serde_json; + +use crate::{ + r_print, + systems::cmd::{ + errors::CmdRenderError, + renderer::{JVRenderResult, JVResultRenderer}, + }, +}; + +pub struct JVResultJsonRenderer; + +impl JVResultRenderer for JVResultJsonRenderer +where + T: Serialize + Sync, +{ + async fn render(data: &T) -> Result { + let mut r = JVRenderResult::default(); + let json_string = serde_json::to_string(data) + .map_err(|e| CmdRenderError::SerializeFailed(e.to_string()))?; + + r_print!(r, "{}", json_string); + + Ok(r) + } +} diff --git a/src/renderers/json_pretty.rs b/src/renderers/json_pretty.rs new file mode 100644 index 0000000..a4a3ba5 --- /dev/null +++ b/src/renderers/json_pretty.rs @@ -0,0 +1,26 @@ +use serde::Serialize; + +use crate::{ + r_print, + systems::cmd::{ + errors::CmdRenderError, + renderer::{JVRenderResult, JVResultRenderer}, + }, +}; + +pub struct JVResultPrettyJsonRenderer; + +impl JVResultRenderer for JVResultPrettyJsonRenderer +where + T: Serialize + Sync, +{ + async fn render(data: &T) -> Result { + let mut r = JVRenderResult::default(); + let json_string = serde_json::to_string_pretty(data) + .map_err(|e| CmdRenderError::SerializeFailed(e.to_string()))?; + + r_print!(r, "{}", json_string); + + Ok(r) + } +} diff --git a/src/systems.rs b/src/systems.rs new file mode 100644 index 0000000..52958ec --- /dev/null +++ b/src/systems.rs @@ -0,0 +1 @@ +pub mod cmd; diff --git a/src/systems/cmd.rs b/src/systems/cmd.rs new file mode 100644 index 0000000..8983892 --- /dev/null +++ b/src/systems/cmd.rs @@ -0,0 +1,5 @@ +pub mod _registry; +pub mod cmd_system; +pub mod errors; +pub mod processer; +pub mod renderer; diff --git a/src/systems/cmd/cmd_system.rs b/src/systems/cmd/cmd_system.rs new file mode 100644 index 0000000..65f972d --- /dev/null +++ b/src/systems/cmd/cmd_system.rs @@ -0,0 +1,100 @@ +use serde::Serialize; + +use crate::{ + r_println, + systems::cmd::{ + errors::{CmdExecuteError, CmdPrepareError, CmdProcessError, CmdRenderError}, + 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; + + /// Process the command with a specified renderer, performing any necessary post-execution processing + fn process_with_renderer_flag( + args: Vec, + ctx: JVCommandContext, + renderer: String, + ) -> impl Future> + Send + where + Self: Sync, + { + async move { + let renderer_str = renderer.as_str(); + include!("_renderers.rs") + } + } + + /// performing any necessary post-execution processing + fn process( + args: Vec, + ctx: JVCommandContext, + ) -> impl Future> + Send + 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 + 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; + + /// Run the command phase, + /// returning an output structure, waiting for rendering + fn exec(input: Input) -> impl Future> + Send; +} diff --git a/src/systems/cmd/errors.rs b/src/systems/cmd/errors.rs new file mode 100644 index 0000000..358d15a --- /dev/null +++ b/src/systems/cmd/errors.rs @@ -0,0 +1,139 @@ +use just_enough_vcs::vcs::data::{member::MemberId, sheet::SheetName}; + +#[derive(thiserror::Error, Debug)] +pub enum CmdPrepareError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("{0}")] + Error(String), + + #[error("LocalWorkspace not found")] + LocalWorkspaceNotFound, + + #[error("LocalConfig not found")] + LocalConfigNotFound, + + #[error("LatestInfo not found")] + LatestInfoNotFound, + + #[error("LatestFileData of {0} not found")] + LatestFileDataNotExist(MemberId), + + #[error("CachedSheet `{0}` not found")] + CachedSheetNotFound(SheetName), + + #[error("LocalSheet `{0}/{1}` not found")] + LocalSheetNotFound(MemberId, SheetName), + + #[error("LocalStatusAnalyzeFailed")] + LocalStatusAnalyzeFailed, + + #[error("No sheet in use")] + NoSheetInUse, +} + +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), + + #[error("Serialize failed, {0}")] + SerializeFailed(String), + + #[error("Renderer `{0}` not found")] + RendererNotFound(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/systems/cmd/processer.rs b/src/systems/cmd/processer.rs new file mode 100644 index 0000000..d357e44 --- /dev/null +++ b/src/systems/cmd/processer.rs @@ -0,0 +1,41 @@ +use crate::systems::cmd::_registry::{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; + +pub async fn jv_cmd_process( + args: Vec, + ctx: JVCommandContext, + renderer_override: String, +) -> 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, renderer_override).await; + } + _ => { + // Multiple matching nodes found + return Err(CmdProcessError::AmbiguousCommand( + matching_nodes + .iter() + .map(|s| s.to_string()) + .collect::>(), + )); + } + } +} diff --git a/src/systems/cmd/renderer.rs b/src/systems/cmd/renderer.rs new file mode 100644 index 0000000..bdd702d --- /dev/null +++ b/src/systems/cmd/renderer.rs @@ -0,0 +1,54 @@ +use std::fmt::{Display, Formatter}; + +use serde::Serialize; + +use crate::systems::cmd::errors::CmdRenderError; + +pub trait JVResultRenderer +where + Data: Serialize, +{ + fn render( + data: &Data, + ) -> impl Future> + Send + Sync; +} + +#[derive(Default, Debug, PartialEq)] +pub struct JVRenderResult { + render_text: String, +} + +impl Display for JVRenderResult { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}\n", self.render_text.trim()) + } +} + +impl JVRenderResult { + pub fn print(&mut self, text: &str) { + self.render_text.push_str(text); + } + + pub fn println(&mut self, text: &str) { + self.render_text.push_str(text); + self.render_text.push('\n'); + } + + pub fn clear(&mut self) { + self.render_text.clear(); + } +} + +#[macro_export] +macro_rules! r_print { + ($result:expr, $($arg:tt)*) => { + $result.print(&format!($($arg)*)); + }; +} + +#[macro_export] +macro_rules! r_println { + ($result:expr, $($arg:tt)*) => { + $result.println(&format!($($arg)*)); + }; +} diff --git a/src/utils/workspace_reader.rs b/src/utils/workspace_reader.rs index 575ec64..a3bc754 100644 --- a/src/utils/workspace_reader.rs +++ b/src/utils/workspace_reader.rs @@ -20,7 +20,7 @@ use just_enough_vcs::{ }, }; -use crate::cmd::errors::CmdPrepareError; +use crate::systems::cmd::errors::CmdPrepareError; /// Temporarily enter a directory to execute a block of code, then return to the original directory macro_rules! entry_dir { -- cgit