From 0488b2f497276c5fbc6b6ac0e4699346a3a63baf Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Fri, 28 Jun 2024 23:53:00 +0200 Subject: [PATCH] refactor(cli::repl): introduce custom error type, modularize --- members/libpt-cli/Cargo.toml | 1 + members/libpt-cli/examples/repl.rs | 23 +++- members/libpt-cli/src/repl/default.rs | 190 ++++++++++++++++++++++++++ members/libpt-cli/src/repl/error.rs | 9 ++ members/libpt-cli/src/repl/mod.rs | 176 +----------------------- 5 files changed, 224 insertions(+), 175 deletions(-) create mode 100644 members/libpt-cli/src/repl/default.rs create mode 100644 members/libpt-cli/src/repl/error.rs diff --git a/members/libpt-cli/Cargo.toml b/members/libpt-cli/Cargo.toml index fbf5f6e..0900f7d 100644 --- a/members/libpt-cli/Cargo.toml +++ b/members/libpt-cli/Cargo.toml @@ -29,3 +29,4 @@ libpt-log = { workspace = true, optional = true } log = { version = "0.4.21", optional = true } shlex = "1.3.0" strum = { version = "0.26.3", features = ["derive"] } +thiserror.workspace = true diff --git a/members/libpt-cli/examples/repl.rs b/members/libpt-cli/examples/repl.rs index 0289149..34bf4fa 100644 --- a/members/libpt-cli/examples/repl.rs +++ b/members/libpt-cli/examples/repl.rs @@ -1,3 +1,4 @@ +use console::style; use libpt_cli::repl::{DefaultRepl, Repl}; use libpt_cli::{clap, printing, strum}; use libpt_log::{debug, Level, Logger}; @@ -5,6 +6,7 @@ use libpt_log::{debug, Level, Logger}; use clap::Subcommand; use strum::EnumIter; +// this is where you define what data/commands/arguments the REPL accepts #[derive(Subcommand, Debug, EnumIter, Clone)] enum ReplCommand { /// wait for LEN seconds @@ -31,19 +33,28 @@ fn main() -> anyhow::Result<()> { // omitted here for brevity let _logger = Logger::builder() .show_time(false) - .max_level(Level::DEBUG) - .build(); + .max_level(Level::INFO) + .build()?; // the compiler can infer that we want to use the ReplCommand enum. - let mut repl = DefaultRepl::::new(); + let mut repl = DefaultRepl::::default(); debug!("entering the repl"); loop { // repl.step() should be at the start of your loop + // It is here that the repl will get the user input, validate it, and so on match repl.step() { Ok(c) => c, Err(e) => { - println!("{e}"); + // if the user requested the help, print in blue, otherwise in red as it's just an + // error + if let libpt_cli::repl::error::ReplError::Parsing(e) = &e { + if e.kind() == clap::error::ErrorKind::DisplayHelp { + println!("{}", style(e).cyan()); + continue; + } + } + println!("{}", style(e).red().bold()); continue; } }; @@ -63,9 +74,9 @@ fn main() -> anyhow::Result<()> { ReplCommand::Hello => println!("Hello!"), ReplCommand::Echo { text, fancy } => { if !fancy { - println!("{}", text.concat()) + println!("{}", text.join(" ")) } else { - printing::blockprint(text.concat(), console::Color::Cyan) + printing::blockprint(text.join(" "), console::Color::Cyan) } } } diff --git a/members/libpt-cli/src/repl/default.rs b/members/libpt-cli/src/repl/default.rs new file mode 100644 index 0000000..3096167 --- /dev/null +++ b/members/libpt-cli/src/repl/default.rs @@ -0,0 +1,190 @@ +use std::fmt::Debug; + +use super::Repl; + +use clap::{Parser, Subcommand}; +use dialoguer::{BasicHistory, Completion}; +use libpt_log::trace; + +#[derive(Parser)] +#[command(multicall = true)] +pub struct DefaultRepl +where + C: Debug, + C: Subcommand, + C: strum::IntoEnumIterator, +{ + /// the command you want to execute, along with its arguments + #[command(subcommand)] + command: Option, + + // the following fields are not to be parsed from a command, but used for the internal workings + // of the repl + #[clap(skip)] + buf: String, + #[clap(skip)] + buf_preparsed: Vec, + #[clap(skip)] + completion: DefaultReplCompletion, + #[clap(skip)] + history: BasicHistory, +} + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)] +pub struct DefaultReplCompletion +where + C: Debug, + C: Subcommand, + C: strum::IntoEnumIterator, +{ + commands: std::marker::PhantomData, +} + +impl Repl for DefaultRepl +where + C: Debug, + C: Subcommand, + C: strum::IntoEnumIterator, +{ + fn new() -> Self { + Self { + command: None, + buf_preparsed: Vec::new(), + buf: String::new(), + history: BasicHistory::new(), + completion: DefaultReplCompletion::new(), + } + } + fn command(&self) -> &Option { + &self.command + } + #[allow(refining_impl_trait)] + fn completion() -> DefaultReplCompletion { + DefaultReplCompletion { + commands: std::marker::PhantomData::, + } + } + fn step(&mut self) -> Result<(), super::error::ReplError> { + self.buf.clear(); + + // NOTE: display::Input requires some kind of lifetime that would be a bother to store in + // our struct. It's documentation also uses it in place, so it should be fine to do it like + // this. + // + // NOTE: It would be nice if we could use the Validator mechanism of dialoguer, but + // unfortunately we can only process our input after we've preparsed it and we need an + // actual output. If we could set a status after the Input is over that would be amazing, + // but that is currently not supported by dialoguer. + // Therefore, every prompt will show as success regardless. + self.buf = dialoguer::Input::with_theme(&dialoguer::theme::ColorfulTheme::default()) + .completion_with(&self.completion) + .history_with(&mut self.history) + .interact_text()?; + + self.buf_preparsed = Vec::new(); + self.buf_preparsed + .extend(shlex::split(&self.buf).unwrap_or_default()); + + trace!("read input: {:?}", self.buf_preparsed); + trace!("repl after step: {:#?}", self); + + // HACK: find a way to not allocate a new struct for this + let cmds = Self::try_parse_from(&self.buf_preparsed)?; + self.command = cmds.command; + Ok(()) + } +} + +impl Default for DefaultRepl +where + C: Debug, + C: Subcommand, + C: strum::IntoEnumIterator, +{ + fn default() -> Self { + Self::new() + } +} + +impl Debug for DefaultRepl +where + C: Debug, + C: Subcommand, + C: strum::IntoEnumIterator, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DefaultRepl") + .field("command", &self.command) + .field("buf", &self.buf) + .field("buf_preparsed", &self.buf_preparsed) + .field("completion", &self.completion) + .field("history", &"(no debug)") + .finish() + } +} + +impl DefaultReplCompletion +where + C: Debug, + C: Subcommand, + C: strum::IntoEnumIterator, +{ + pub fn new() -> Self { + Self { + commands: std::marker::PhantomData::, + } + } + fn commands(&self) -> Vec { + let mut buf = Vec::new(); + // every crate has the help command, but it is not part of the enum + buf.push("help".to_string()); + for c in C::iter() { + // HACK: this is a horrible way to do this + // I just need the names of the commands + buf.push( + format!("{c:?}") + .split_whitespace() + .map(|e| e.to_lowercase()) + .next() + .unwrap() + .to_string(), + ) + } + trace!("commands: {buf:?}"); + buf + } +} + +impl Default for DefaultReplCompletion +where + C: Debug, + C: Subcommand, + C: strum::IntoEnumIterator, +{ + fn default() -> Self { + Self::new() + } +} + +impl Completion for DefaultReplCompletion +where + C: Debug, + C: Subcommand, + C: strum::IntoEnumIterator, +{ + /// Simple completion implementation based on substring + fn get(&self, input: &str) -> Option { + let matches = self + .commands() + .into_iter() + .filter(|option| option.starts_with(input)) + .collect::>(); + + trace!("\nmatches: {matches:#?}"); + if matches.len() == 1 { + Some(matches[0].to_string()) + } else { + None + } + } +} diff --git a/members/libpt-cli/src/repl/error.rs b/members/libpt-cli/src/repl/error.rs new file mode 100644 index 0000000..c4416a3 --- /dev/null +++ b/members/libpt-cli/src/repl/error.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ReplError { + #[error(transparent)] + Parsing(#[from] clap::Error), + #[error(transparent)] + Input(#[from] dialoguer::Error) +} diff --git a/members/libpt-cli/src/repl/mod.rs b/members/libpt-cli/src/repl/mod.rs index 52dcc6d..87a95eb 100644 --- a/members/libpt-cli/src/repl/mod.rs +++ b/members/libpt-cli/src/repl/mod.rs @@ -1,102 +1,12 @@ use std::fmt::Debug; +pub mod error; +use error::ReplError; +mod default; +pub use default::*; + use clap::{Parser, Subcommand}; -use dialoguer::{BasicHistory, Completion}; -use libpt_log::trace; - -#[derive(Parser)] -#[command(multicall = true)] -pub struct DefaultRepl -where - C: Debug, - C: Subcommand, - C: strum::IntoEnumIterator, -{ - /// the command you want to execute, along with its arguments - #[command(subcommand)] - command: Option, - - // the following fields are not to be parsed from a command, but used for the internal workings - // of the repl - #[clap(skip)] - buf: String, - #[clap(skip)] - buf_preparsed: Vec, - #[clap(skip)] - completion: DefaultReplCompletion, - #[clap(skip)] - history: BasicHistory, -} - -impl Debug for DefaultRepl -where - C: Debug, - C: Subcommand, - C: strum::IntoEnumIterator, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DefaultRepl") - .field("command", &self.command) - .field("buf", &self.buf) - .field("buf_preparsed", &self.buf_preparsed) - .field("completion", &self.completion) - .field("history", &"(no debug)") - .finish() - } -} - -#[derive(Debug)] -pub struct DefaultReplCompletion -where - C: Debug, - C: Subcommand, - C: strum::IntoEnumIterator, -{ - commands: std::marker::PhantomData, -} - -impl DefaultReplCompletion -where - C: Debug, - C: Subcommand, - C: strum::IntoEnumIterator, -{ - pub fn new() -> Self { - Self { - commands: std::marker::PhantomData::, - } - } - fn commands(&self) -> Vec { - let mut buf = Vec::new(); - // every crate has the help command, but it is not part of the enum - buf.push("help".to_string()); - for c in C::iter() { - // HACK: this is a horrible way to do this - // I just need the names of the commands - buf.push( - format!("{c:?}") - .split_whitespace() - .map(|e| e.to_lowercase()) - .next() - .unwrap() - .to_string(), - ) - } - trace!("commands: {buf:?}"); - buf - } -} - -impl Default for DefaultReplCompletion -where - C: Debug, - C: Subcommand, - C: strum::IntoEnumIterator, -{ - fn default() -> Self { - Self::new() - } -} +use dialoguer::Completion; pub trait Repl: Parser + Debug where @@ -115,77 +25,5 @@ where /// advance the repl to the next iteration of the main loop /// /// This should be used at the start of your loop - fn step(&mut self) -> anyhow::Result<()>; -} - -impl Completion for DefaultReplCompletion -where - C: Debug, - C: Subcommand, - C: strum::IntoEnumIterator, -{ - /// Simple completion implementation based on substring - fn get(&self, input: &str) -> Option { - let matches = self - .commands() - .into_iter() - .filter(|option| option.starts_with(input)) - .collect::>(); - - trace!("\nmatches: {matches:#?}"); - if matches.len() == 1 { - Some(matches[0].to_string()) - } else { - None - } - } -} - -impl Repl for DefaultRepl -where - C: Debug, - C: Subcommand, - C: strum::IntoEnumIterator, -{ - fn new() -> Self { - Self { - command: None, - buf_preparsed: Vec::new(), - buf: String::new(), - history: BasicHistory::new(), - completion: DefaultReplCompletion::new(), - } - } - fn command(&self) -> &Option { - &self.command - } - #[allow(refining_impl_trait)] - fn completion() -> DefaultReplCompletion { - DefaultReplCompletion { - commands: std::marker::PhantomData::, - } - } - fn step(&mut self) -> anyhow::Result<()> { - self.buf.clear(); - - // NOTE: display::Input requires some kind of lifetime that would be a bother to store in - // our struct. It's documentation also uses it in place, so it should be fine to do it like - // this. - self.buf = dialoguer::Input::with_theme(&dialoguer::theme::ColorfulTheme::default()) - .completion_with(&self.completion) - .history_with(&mut self.history) - .interact_text()?; - - self.buf_preparsed = Vec::new(); - self.buf_preparsed - .extend(shlex::split(&self.buf).unwrap_or_default()); - - trace!("read input: {:?}", self.buf_preparsed); - trace!("repl after step: {:#?}", self); - - // HACK: find a way to not allocate a new struct for this - let cmds = Self::try_parse_from(&self.buf_preparsed)?; - self.command = cmds.command; - Ok(()) - } + fn step(&mut self) -> Result<(), ReplError>; }