diff --git a/Cargo.lock b/Cargo.lock index 4f2d56f7d..7b1169570 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7869,7 +7869,6 @@ dependencies = [ name = "reth-storage-errors" version = "0.2.0-beta.8" dependencies = [ - "clap", "reth-fs-util", "reth-primitives", "thiserror", diff --git a/crates/node-core/Cargo.toml b/crates/node-core/Cargo.toml index 0f2435565..dee84f904 100644 --- a/crates/node-core/Cargo.toml +++ b/crates/node-core/Cargo.toml @@ -16,7 +16,7 @@ reth-primitives.workspace = true reth-fs-util.workspace = true reth-db = { workspace = true, features = ["mdbx"] } reth-db-api.workspace = true -reth-storage-errors = { workspace = true, features = ["clap"] } +reth-storage-errors.workspace = true reth-provider.workspace = true reth-network = { workspace = true, features = ["serde"] } reth-network-p2p.workspace = true diff --git a/crates/node-core/src/args/database.rs b/crates/node-core/src/args/database.rs index 77a458cb3..09e9de82f 100644 --- a/crates/node-core/src/args/database.rs +++ b/crates/node-core/src/args/database.rs @@ -1,7 +1,11 @@ //! clap [Args](clap::Args) for database configuration use crate::version::default_client_version; -use clap::Args; +use clap::{ + builder::{PossibleValue, TypedValueParser}, + error::ErrorKind, + Arg, Args, Command, Error, +}; use reth_storage_errors::db::LogLevel; /// Parameters for database configuration @@ -9,7 +13,7 @@ use reth_storage_errors::db::LogLevel; #[command(next_help_heading = "Database")] pub struct DatabaseArgs { /// Database logging level. Levels higher than "notice" require a debug build. - #[arg(long = "db.log-level", value_enum)] + #[arg(long = "db.log-level", value_parser = LogLevelValueParser::default())] pub log_level: Option, /// Open environment in exclusive/monopolistic mode. Makes it possible to open a database on an /// NFS volume. @@ -26,6 +30,44 @@ impl DatabaseArgs { } } +/// clap value parser for [`LogLevel`]. +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +struct LogLevelValueParser; + +impl TypedValueParser for LogLevelValueParser { + type Value = LogLevel; + + fn parse_ref( + &self, + _cmd: &Command, + arg: Option<&Arg>, + value: &std::ffi::OsStr, + ) -> Result { + let val = + value.to_str().ok_or_else(|| Error::raw(ErrorKind::InvalidUtf8, "Invalid UTF-8"))?; + + val.parse::().map_err(|err| { + let arg = arg.map(|a| a.to_string()).unwrap_or_else(|| "...".to_owned()); + let possible_values = LogLevel::value_variants() + .iter() + .map(|v| format!("- {:?}: {}", v, v.help_message())) + .collect::>() + .join("\n"); + let msg = format!( + "Invalid value '{val}' for {arg}: {err}.\n Possible values:\n{possible_values}" + ); + clap::Error::raw(clap::error::ErrorKind::InvalidValue, msg) + }) + } + + fn possible_values(&self) -> Option + '_>> { + let values = LogLevel::value_variants() + .iter() + .map(|v| PossibleValue::new(v.variant_name()).help(v.help_message())); + Some(Box::new(values)) + } +} #[cfg(test)] mod tests { use super::*; @@ -39,9 +81,60 @@ mod tests { } #[test] - fn test_parse_database_args() { + fn test_default_database_args() { let default_args = DatabaseArgs::default(); let args = CommandParser::::parse_from(["reth"]).args; assert_eq!(args, default_args); } + + #[test] + fn test_possible_values() { + // Initialize the LogLevelValueParser + let parser = LogLevelValueParser; + + // Call the possible_values method + let possible_values: Vec = parser.possible_values().unwrap().collect(); + + // Expected possible values + let expected_values = vec![ + PossibleValue::new("fatal") + .help("Enables logging for critical conditions, i.e. assertion failures"), + PossibleValue::new("error").help("Enables logging for error conditions"), + PossibleValue::new("warn").help("Enables logging for warning conditions"), + PossibleValue::new("notice") + .help("Enables logging for normal but significant condition"), + PossibleValue::new("verbose").help("Enables logging for verbose informational"), + PossibleValue::new("debug").help("Enables logging for debug-level messages"), + PossibleValue::new("trace").help("Enables logging for trace debug-level messages"), + PossibleValue::new("extra").help("Enables logging for extra debug-level messages"), + ]; + + // Check that the possible values match the expected values + assert_eq!(possible_values.len(), expected_values.len()); + for (actual, expected) in possible_values.iter().zip(expected_values.iter()) { + assert_eq!(actual.get_name(), expected.get_name()); + assert_eq!(actual.get_help(), expected.get_help()); + } + } + + #[test] + fn test_command_parser_with_valid_log_level() { + let cmd = + CommandParser::::try_parse_from(["reth", "--db.log-level", "Debug"]) + .unwrap(); + assert_eq!(cmd.args.log_level, Some(LogLevel::Debug)); + } + + #[test] + fn test_command_parser_with_invalid_log_level() { + let result = + CommandParser::::try_parse_from(["reth", "--db.log-level", "invalid"]); + assert!(result.is_err()); + } + + #[test] + fn test_command_parser_without_log_level() { + let cmd = CommandParser::::try_parse_from(["reth"]).unwrap(); + assert_eq!(cmd.args.log_level, None); + } } diff --git a/crates/storage/errors/Cargo.toml b/crates/storage/errors/Cargo.toml index c1ce595ea..5fa806345 100644 --- a/crates/storage/errors/Cargo.toml +++ b/crates/storage/errors/Cargo.toml @@ -15,7 +15,5 @@ reth-primitives.workspace = true reth-fs-util.workspace = true thiserror.workspace = true -clap = { workspace = true, features = ["derive"], optional = true } -[features] -clap = ["dep:clap"] \ No newline at end of file + diff --git a/crates/storage/errors/src/db.rs b/crates/storage/errors/src/db.rs index 33f8b13c0..b731e4d24 100644 --- a/crates/storage/errors/src/db.rs +++ b/crates/storage/errors/src/db.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, str::FromStr}; use thiserror::Error; /// Database error type. @@ -103,7 +103,6 @@ pub enum DatabaseWriteOperation { /// Database log level. #[derive(Debug, PartialEq, Eq, Clone, Copy)] -#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum LogLevel { /// Enables logging for critical conditions, i.e. assertion failures. Fatal, @@ -122,3 +121,65 @@ pub enum LogLevel { /// Enables logging for extra debug-level messages. Extra, } + +impl LogLevel { + /// All possible variants of the `LogLevel` enum + pub const fn value_variants() -> &'static [Self] { + &[ + Self::Fatal, + Self::Error, + Self::Warn, + Self::Notice, + Self::Verbose, + Self::Debug, + Self::Trace, + Self::Extra, + ] + } + + /// Static str reference to `LogLevel` enum, required for `Clap::Builder::PossibleValue::new()` + pub const fn variant_name(&self) -> &'static str { + match self { + Self::Fatal => "fatal", + Self::Error => "error", + Self::Warn => "warn", + Self::Notice => "notice", + Self::Verbose => "verbose", + Self::Debug => "debug", + Self::Trace => "trace", + Self::Extra => "extra", + } + } + + /// Returns all variants descriptions + pub const fn help_message(&self) -> &'static str { + match self { + Self::Fatal => "Enables logging for critical conditions, i.e. assertion failures", + Self::Error => "Enables logging for error conditions", + Self::Warn => "Enables logging for warning conditions", + Self::Notice => "Enables logging for normal but significant condition", + Self::Verbose => "Enables logging for verbose informational", + Self::Debug => "Enables logging for debug-level messages", + Self::Trace => "Enables logging for trace debug-level messages", + Self::Extra => "Enables logging for extra debug-level messages", + } + } +} + +impl FromStr for LogLevel { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "fatal" => Ok(Self::Fatal), + "error" => Ok(Self::Error), + "warn" => Ok(Self::Warn), + "notice" => Ok(Self::Notice), + "verbose" => Ok(Self::Verbose), + "debug" => Ok(Self::Debug), + "trace" => Ok(Self::Trace), + "extra" => Ok(Self::Extra), + _ => Err(format!("Invalid log level: {}", s)), + } + } +}