feat: support customizable RPC namespace parsers (#18160)

Co-authored-by: Federico Gimenez <federico.gimenez@gmail.com>
This commit is contained in:
Arsenii Kulikov
2025-09-09 17:17:43 +03:00
committed by GitHub
parent 394a53d7b0
commit 90aa99cb3c
11 changed files with 462 additions and 78 deletions

2
Cargo.lock generated
View File

@@ -8282,6 +8282,7 @@ dependencies = [
"reth-node-core",
"reth-node-ethereum",
"reth-node-metrics",
"reth-rpc-server-types",
"reth-tracing",
"tempfile",
"tracing",
@@ -9221,6 +9222,7 @@ dependencies = [
"reth-primitives-traits",
"reth-provider",
"reth-prune",
"reth-rpc-server-types",
"reth-stages",
"reth-static-file",
"reth-static-file-types",

View File

@@ -21,6 +21,7 @@ reth-node-builder.workspace = true
reth-node-core.workspace = true
reth-node-ethereum.workspace = true
reth-node-metrics.workspace = true
reth-rpc-server-types.workspace = true
reth-tracing.workspace = true
reth-node-api.workspace = true

View File

@@ -18,8 +18,9 @@ use reth_node_builder::{NodeBuilder, WithLaunchContext};
use reth_node_core::{args::LogArgs, version::version_metadata};
use reth_node_ethereum::{consensus::EthBeaconConsensus, EthEvmConfig, EthereumNode};
use reth_node_metrics::recorder::install_prometheus_recorder;
use reth_rpc_server_types::{DefaultRpcModuleValidator, RpcModuleValidator};
use reth_tracing::FileWorkerGuard;
use std::{ffi::OsString, fmt, future::Future, sync::Arc};
use std::{ffi::OsString, fmt, future::Future, marker::PhantomData, sync::Arc};
use tracing::info;
/// The main reth cli interface.
@@ -27,8 +28,11 @@ use tracing::info;
/// This is the entrypoint to the executable.
#[derive(Debug, Parser)]
#[command(author, version =version_metadata().short_version.as_ref(), long_version = version_metadata().long_version.as_ref(), about = "Reth", long_about = None)]
pub struct Cli<C: ChainSpecParser = EthereumChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs>
{
pub struct Cli<
C: ChainSpecParser = EthereumChainSpecParser,
Ext: clap::Args + fmt::Debug = NoArgs,
Rpc: RpcModuleValidator = DefaultRpcModuleValidator,
> {
/// The command to run
#[command(subcommand)]
pub command: Commands<C, Ext>,
@@ -36,6 +40,10 @@ pub struct Cli<C: ChainSpecParser = EthereumChainSpecParser, Ext: clap::Args + f
/// The logging configuration for the CLI.
#[command(flatten)]
pub logs: LogArgs,
/// Type marker for the RPC module validator
#[arg(skip)]
pub _phantom: PhantomData<Rpc>,
}
impl Cli {
@@ -54,7 +62,7 @@ impl Cli {
}
}
impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> Cli<C, Ext> {
impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, Rpc: RpcModuleValidator> Cli<C, Ext, Rpc> {
/// Execute the configured cli command.
///
/// This accepts a closure that is used to launch the node via the
@@ -190,9 +198,20 @@ impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> Cli<C, Ext> {
let _ = install_prometheus_recorder();
match self.command {
Commands::Node(command) => runner.run_command_until_exit(|ctx| {
command.execute(ctx, FnLauncher::new::<C, Ext>(launcher))
}),
Commands::Node(command) => {
// Validate RPC modules using the configured validator
if let Some(http_api) = &command.rpc.http_api {
Rpc::validate_selection(http_api, "http.api")
.map_err(|e| eyre::eyre!("{e}"))?;
}
if let Some(ws_api) = &command.rpc.ws_api {
Rpc::validate_selection(ws_api, "ws.api").map_err(|e| eyre::eyre!("{e}"))?;
}
runner.run_command_until_exit(|ctx| {
command.execute(ctx, FnLauncher::new::<C, Ext>(launcher))
})
}
Commands::Init(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
Commands::InitState(command) => {
runner.run_blocking_until_ctrl_c(command.execute::<N>())
@@ -417,4 +436,72 @@ mod tests {
.unwrap();
assert!(reth.run(async move |_, _| Ok(())).is_ok());
}
#[test]
fn test_rpc_module_validation() {
use reth_rpc_server_types::RethRpcModule;
// Test that standard modules are accepted
let cli =
Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,admin,debug"]).unwrap();
if let Commands::Node(command) = &cli.command {
if let Some(http_api) = &command.rpc.http_api {
// Should contain the expected modules
let modules = http_api.to_selection();
assert!(modules.contains(&RethRpcModule::Eth));
assert!(modules.contains(&RethRpcModule::Admin));
assert!(modules.contains(&RethRpcModule::Debug));
} else {
panic!("Expected http.api to be set");
}
} else {
panic!("Expected Node command");
}
// Test that unknown modules are parsed as Other variant
let cli =
Cli::try_parse_args_from(["reth", "node", "--http.api", "eth,customrpc"]).unwrap();
if let Commands::Node(command) = &cli.command {
if let Some(http_api) = &command.rpc.http_api {
let modules = http_api.to_selection();
assert!(modules.contains(&RethRpcModule::Eth));
assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
} else {
panic!("Expected http.api to be set");
}
} else {
panic!("Expected Node command");
}
}
#[test]
fn test_rpc_module_unknown_rejected() {
use reth_cli_runner::CliRunner;
// Test that unknown module names are rejected during validation
let cli =
Cli::try_parse_args_from(["reth", "node", "--http.api", "unknownmodule"]).unwrap();
// When we try to run the CLI with validation, it should fail
let runner = CliRunner::try_default_runtime().unwrap();
let result = cli.with_runner(runner, |_, _| async { Ok(()) });
assert!(result.is_err());
let err = result.unwrap_err();
let err_msg = err.to_string();
// The error should mention it's an unknown module
assert!(
err_msg.contains("Unknown RPC module"),
"Error should mention unknown module: {}",
err_msg
);
assert!(
err_msg.contains("'unknownmodule'"),
"Error should mention the module name: {}",
err_msg
);
}
}

View File

@@ -407,7 +407,7 @@ impl Default for RpcServerArgs {
}
}
/// clap value parser for [`RpcModuleSelection`].
/// clap value parser for [`RpcModuleSelection`] with configurable validation.
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
struct RpcModuleSelectionValueParser;
@@ -418,23 +418,20 @@ impl TypedValueParser for RpcModuleSelectionValueParser {
fn parse_ref(
&self,
_cmd: &Command,
arg: Option<&Arg>,
_arg: Option<&Arg>,
value: &OsStr,
) -> Result<Self::Value, clap::Error> {
let val =
value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
val.parse::<RpcModuleSelection>().map_err(|err| {
let arg = arg.map(|a| a.to_string()).unwrap_or_else(|| "...".to_owned());
let possible_values = RethRpcModule::all_variant_names().to_vec().join(",");
let msg = format!(
"Invalid value '{val}' for {arg}: {err}.\n [possible values: {possible_values}]"
);
clap::Error::raw(clap::error::ErrorKind::InvalidValue, msg)
})
// This will now accept any module name, creating Other(name) for unknowns
Ok(val
.parse::<RpcModuleSelection>()
.expect("RpcModuleSelection parsing cannot fail with Other variant"))
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
let values = RethRpcModule::all_variant_names().iter().map(PossibleValue::new);
// Only show standard modules in help text (excludes "other")
let values = RethRpcModule::standard_variant_names().map(PossibleValue::new);
Some(Box::new(values))
}
}

View File

@@ -12,8 +12,10 @@ workspace = true
[dependencies]
reth-static-file-types = { workspace = true, features = ["clap"] }
reth-cli.workspace = true
reth-cli-commands.workspace = true
reth-consensus.workspace = true
reth-rpc-server-types.workspace = true
reth-primitives-traits.workspace = true
reth-db = { workspace = true, features = ["mdbx", "op"] }
reth-db-api.workspace = true
@@ -39,7 +41,6 @@ reth-optimism-consensus.workspace = true
reth-chainspec.workspace = true
reth-node-events.workspace = true
reth-optimism-evm.workspace = true
reth-cli.workspace = true
reth-cli-runner.workspace = true
reth-node-builder = { workspace = true, features = ["op"] }
reth-tracing.workspace = true

View File

@@ -7,25 +7,27 @@ use reth_node_metrics::recorder::install_prometheus_recorder;
use reth_optimism_chainspec::OpChainSpec;
use reth_optimism_consensus::OpBeaconConsensus;
use reth_optimism_node::{OpExecutorProvider, OpNode};
use reth_rpc_server_types::RpcModuleValidator;
use reth_tracing::{FileWorkerGuard, Layers};
use std::{fmt, sync::Arc};
use tracing::info;
/// A wrapper around a parsed CLI that handles command execution.
#[derive(Debug)]
pub struct CliApp<Spec: ChainSpecParser, Ext: clap::Args + fmt::Debug> {
cli: Cli<Spec, Ext>,
pub struct CliApp<Spec: ChainSpecParser, Ext: clap::Args + fmt::Debug, Rpc: RpcModuleValidator> {
cli: Cli<Spec, Ext, Rpc>,
runner: Option<CliRunner>,
layers: Option<Layers>,
guard: Option<FileWorkerGuard>,
}
impl<C, Ext> CliApp<C, Ext>
impl<C, Ext, Rpc> CliApp<C, Ext, Rpc>
where
C: ChainSpecParser<ChainSpec = OpChainSpec>,
Ext: clap::Args + fmt::Debug,
Rpc: RpcModuleValidator,
{
pub(crate) fn new(cli: Cli<C, Ext>) -> Self {
pub(crate) fn new(cli: Cli<C, Ext, Rpc>) -> Self {
Self { cli, runner: None, layers: Some(Layers::new()), guard: None }
}
@@ -71,6 +73,14 @@ where
match self.cli.command {
Commands::Node(command) => {
// Validate RPC modules using the configured validator
if let Some(http_api) = &command.rpc.http_api {
Rpc::validate_selection(http_api, "http.api").map_err(|e| eyre!("{e}"))?;
}
if let Some(ws_api) = &command.rpc.ws_api {
Rpc::validate_selection(ws_api, "ws.api").map_err(|e| eyre!("{e}"))?;
}
runner.run_command_until_exit(|ctx| command.execute(ctx, launcher))
}
Commands::Init(command) => {

View File

@@ -35,8 +35,9 @@ pub mod ovm_file_codec;
pub use app::CliApp;
pub use commands::{import::ImportOpCommand, import_receipts::ImportReceiptsOpCommand};
use reth_optimism_chainspec::OpChainSpec;
use reth_rpc_server_types::{DefaultRpcModuleValidator, RpcModuleValidator};
use std::{ffi::OsString, fmt, sync::Arc};
use std::{ffi::OsString, fmt, marker::PhantomData, sync::Arc};
use chainspec::OpChainSpecParser;
use clap::{command, Parser};
@@ -59,8 +60,11 @@ use reth_node_metrics as _;
/// This is the entrypoint to the executable.
#[derive(Debug, Parser)]
#[command(author, version = version_metadata().short_version.as_ref(), long_version = version_metadata().long_version.as_ref(), about = "Reth", long_about = None)]
pub struct Cli<Spec: ChainSpecParser = OpChainSpecParser, Ext: clap::Args + fmt::Debug = RollupArgs>
{
pub struct Cli<
Spec: ChainSpecParser = OpChainSpecParser,
Ext: clap::Args + fmt::Debug = RollupArgs,
Rpc: RpcModuleValidator = DefaultRpcModuleValidator,
> {
/// The command to run
#[command(subcommand)]
pub command: Commands<Spec, Ext>,
@@ -68,6 +72,10 @@ pub struct Cli<Spec: ChainSpecParser = OpChainSpecParser, Ext: clap::Args + fmt:
/// The logging configuration for the CLI.
#[command(flatten)]
pub logs: LogArgs,
/// Type marker for the RPC module validator
#[arg(skip)]
_phantom: PhantomData<Rpc>,
}
impl Cli {
@@ -86,16 +94,17 @@ impl Cli {
}
}
impl<C, Ext> Cli<C, Ext>
impl<C, Ext, Rpc> Cli<C, Ext, Rpc>
where
C: ChainSpecParser<ChainSpec = OpChainSpec>,
Ext: clap::Args + fmt::Debug,
Rpc: RpcModuleValidator,
{
/// Configures the CLI and returns a [`CliApp`] instance.
///
/// This method is used to prepare the CLI for execution by wrapping it in a
/// [`CliApp`] that can be further configured before running.
pub fn configure(self) -> CliApp<C, Ext> {
pub fn configure(self) -> CliApp<C, Ext, Rpc> {
CliApp::new(self)
}

View File

@@ -24,7 +24,11 @@ pub mod primitives {
#[cfg(feature = "cli")]
pub mod cli {
#[doc(inline)]
pub use reth_cli_util::*;
pub use reth_cli_util::{
allocator, get_secret_key, hash_or_num_value_parser, load_secret_key,
parse_duration_from_secs, parse_duration_from_secs_or_ms, parse_ether_value,
parse_socket_address, sigsegv_handler,
};
#[doc(inline)]
pub use reth_optimism_cli::*;
}

View File

@@ -919,11 +919,10 @@ where
let namespaces: Vec<_> = namespaces.collect();
namespaces
.iter()
.copied()
.map(|namespace| {
self.modules
.entry(namespace)
.or_insert_with(|| match namespace {
.entry(namespace.clone())
.or_insert_with(|| match namespace.clone() {
RethRpcModule::Admin => {
AdminApi::new(self.network.clone(), self.provider.chain_spec())
.into_rpc()
@@ -985,7 +984,9 @@ where
// only relevant for Ethereum and configured in `EthereumAddOns`
// implementation
// TODO: can we get rid of this here?
RethRpcModule::Flashbots => Default::default(),
// Custom modules are not handled here - they should be registered via
// extend_rpc_modules
RethRpcModule::Flashbots | RethRpcModule::Other(_) => Default::default(),
RethRpcModule::Miner => MinerApi::default().into_rpc().into(),
RethRpcModule::Mev => {
EthSimBundle::new(eth_api.clone(), self.blocking_pool_guard.clone())
@@ -1574,9 +1575,9 @@ impl TransportRpcModuleConfig {
let ws_modules =
self.ws.as_ref().map(RpcModuleSelection::to_selection).unwrap_or_default();
let http_not_ws = http_modules.difference(&ws_modules).copied().collect();
let ws_not_http = ws_modules.difference(&http_modules).copied().collect();
let overlap = http_modules.intersection(&ws_modules).copied().collect();
let http_not_ws = http_modules.difference(&ws_modules).cloned().collect();
let ws_not_http = ws_modules.difference(&http_modules).cloned().collect();
let overlap = http_modules.intersection(&ws_modules).cloned().collect();
Err(WsHttpSamePortError::ConflictingModules(Box::new(ConflictingModules {
overlap,
@@ -1712,7 +1713,7 @@ impl TransportRpcModules {
/// Returns all unique endpoints installed for the given module.
///
/// Note: In case of duplicate method names this only record the first occurrence.
pub fn methods_by_module<F>(&self, module: RethRpcModule) -> Methods {
pub fn methods_by_module(&self, module: RethRpcModule) -> Methods {
self.methods_by(|name| name.starts_with(module.as_str()))
}

View File

@@ -13,6 +13,9 @@ pub mod constants;
pub mod result;
mod module;
pub use module::{RethRpcModule, RpcModuleSelection};
pub use module::{
DefaultRpcModuleValidator, LenientRpcModuleValidator, RethRpcModule, RpcModuleSelection,
RpcModuleValidator,
};
pub use result::ToRpcResult;

View File

@@ -1,7 +1,7 @@
use std::{collections::HashSet, fmt, str::FromStr};
use serde::{Deserialize, Serialize, Serializer};
use strum::{AsRefStr, EnumIter, IntoStaticStr, ParseError, VariantArray, VariantNames};
use strum::{ParseError, VariantNames};
/// Describes the modules that should be installed.
///
@@ -107,8 +107,8 @@ impl RpcModuleSelection {
pub fn iter_selection(&self) -> Box<dyn Iterator<Item = RethRpcModule> + '_> {
match self {
Self::All => Box::new(RethRpcModule::modules().into_iter()),
Self::Standard => Box::new(Self::STANDARD_MODULES.iter().copied()),
Self::Selection(s) => Box::new(s.iter().copied()),
Self::Standard => Box::new(Self::STANDARD_MODULES.iter().cloned()),
Self::Selection(s) => Box::new(s.iter().cloned()),
}
}
@@ -228,7 +228,7 @@ impl From<HashSet<RethRpcModule>> for RpcModuleSelection {
impl From<&[RethRpcModule]> for RpcModuleSelection {
fn from(s: &[RethRpcModule]) -> Self {
Self::Selection(s.iter().copied().collect())
Self::Selection(s.iter().cloned().collect())
}
}
@@ -240,7 +240,7 @@ impl From<Vec<RethRpcModule>> for RpcModuleSelection {
impl<const N: usize> From<[RethRpcModule; N]> for RpcModuleSelection {
fn from(s: [RethRpcModule; N]) -> Self {
Self::Selection(s.iter().copied().collect())
Self::Selection(s.iter().cloned().collect())
}
}
@@ -249,7 +249,7 @@ impl<'a> FromIterator<&'a RethRpcModule> for RpcModuleSelection {
where
I: IntoIterator<Item = &'a RethRpcModule>,
{
iter.into_iter().copied().collect()
iter.into_iter().cloned().collect()
}
}
@@ -293,20 +293,7 @@ impl fmt::Display for RpcModuleSelection {
}
/// Represents RPC modules that are supported by reth
#[derive(
Debug,
Clone,
Copy,
Eq,
PartialEq,
Hash,
AsRefStr,
IntoStaticStr,
VariantNames,
VariantArray,
EnumIter,
Deserialize,
)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, VariantNames, Deserialize)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "kebab-case")]
pub enum RethRpcModule {
@@ -336,36 +323,90 @@ pub enum RethRpcModule {
Miner,
/// `mev_` module
Mev,
/// Custom RPC module not part of the standard set
#[strum(default)]
#[serde(untagged)]
Other(String),
}
// === impl RethRpcModule ===
impl RethRpcModule {
/// Returns the number of variants in the enum
/// All standard variants (excludes Other)
const STANDARD_VARIANTS: &'static [Self] = &[
Self::Admin,
Self::Debug,
Self::Eth,
Self::Net,
Self::Trace,
Self::Txpool,
Self::Web3,
Self::Rpc,
Self::Reth,
Self::Ots,
Self::Flashbots,
Self::Miner,
Self::Mev,
];
/// Returns the number of standard variants (excludes Other)
pub const fn variant_count() -> usize {
<Self as VariantArray>::VARIANTS.len()
Self::STANDARD_VARIANTS.len()
}
/// Returns all variant names of the enum
/// Returns all variant names including Other (for parsing)
pub const fn all_variant_names() -> &'static [&'static str] {
<Self as VariantNames>::VARIANTS
}
/// Returns all variants of the enum
pub const fn all_variants() -> &'static [Self] {
<Self as VariantArray>::VARIANTS
/// Returns standard variant names (excludes "other") for CLI display
pub fn standard_variant_names() -> impl Iterator<Item = &'static str> {
<Self as VariantNames>::VARIANTS.iter().copied().filter(|&name| name != "other")
}
/// Returns all variants of the enum
pub fn modules() -> impl IntoIterator<Item = Self> {
use strum::IntoEnumIterator;
Self::iter()
/// Returns all standard variants (excludes Other)
pub const fn all_variants() -> &'static [Self] {
Self::STANDARD_VARIANTS
}
/// Returns iterator over standard modules only
pub fn modules() -> impl IntoIterator<Item = Self> + Clone {
Self::STANDARD_VARIANTS.iter().cloned()
}
/// Returns the string representation of the module.
#[inline]
pub fn as_str(&self) -> &'static str {
self.into()
pub fn as_str(&self) -> &str {
match self {
Self::Other(s) => s.as_str(),
_ => self.as_ref(), // Uses AsRefStr trait
}
}
/// Returns true if this is an `Other` variant.
pub const fn is_other(&self) -> bool {
matches!(self, Self::Other(_))
}
}
impl AsRef<str> for RethRpcModule {
fn as_ref(&self) -> &str {
match self {
Self::Other(s) => s.as_str(),
// For standard variants, use the derive-generated static strings
Self::Admin => "admin",
Self::Debug => "debug",
Self::Eth => "eth",
Self::Net => "net",
Self::Trace => "trace",
Self::Txpool => "txpool",
Self::Web3 => "web3",
Self::Rpc => "rpc",
Self::Reth => "reth",
Self::Ots => "ots",
Self::Flashbots => "flashbots",
Self::Miner => "miner",
Self::Mev => "mev",
}
}
}
@@ -387,7 +428,8 @@ impl FromStr for RethRpcModule {
"flashbots" => Self::Flashbots,
"miner" => Self::Miner,
"mev" => Self::Mev,
_ => return Err(ParseError::VariantNotFound),
// Any unknown module becomes Other
other => Self::Other(other.to_string()),
})
}
}
@@ -410,7 +452,81 @@ impl Serialize for RethRpcModule {
where
S: Serializer,
{
s.serialize_str(self.as_ref())
s.serialize_str(self.as_str())
}
}
/// Trait for validating RPC module selections.
///
/// This allows customizing how RPC module names are validated when parsing
/// CLI arguments or configuration.
pub trait RpcModuleValidator: Clone + Send + Sync + 'static {
/// Parse and validate an RPC module selection string.
fn parse_selection(s: &str) -> Result<RpcModuleSelection, String>;
/// Validates RPC module selection that was already parsed.
///
/// This is used to validate modules that were parsed as `Other` variants
/// to ensure they meet the validation rules of the specific implementation.
fn validate_selection(modules: &RpcModuleSelection, arg_name: &str) -> Result<(), String> {
// Re-validate the modules using the parser's validator
// This is necessary because the clap value parser accepts any input
// and we need to validate according to the specific parser's rules
let RpcModuleSelection::Selection(module_set) = modules else {
// All or Standard variants are always valid
return Ok(());
};
for module in module_set {
let RethRpcModule::Other(name) = module else {
// Standard modules are always valid
continue;
};
// Try to parse and validate using the configured validator
// This will check for typos and other validation rules
Self::parse_selection(name)
.map_err(|e| format!("Invalid RPC module '{name}' in {arg_name}: {e}"))?;
}
Ok(())
}
}
/// Default validator that rejects unknown module names.
///
/// This validator only accepts known RPC module names.
#[derive(Debug, Clone, Copy)]
pub struct DefaultRpcModuleValidator;
impl RpcModuleValidator for DefaultRpcModuleValidator {
fn parse_selection(s: &str) -> Result<RpcModuleSelection, String> {
// First try standard parsing
let selection = RpcModuleSelection::from_str(s)
.map_err(|e| format!("Failed to parse RPC modules: {}", e))?;
// Validate each module in the selection
if let RpcModuleSelection::Selection(modules) = &selection {
for module in modules {
if let RethRpcModule::Other(name) = module {
return Err(format!("Unknown RPC module: '{}'", name));
}
}
}
Ok(selection)
}
}
/// Lenient validator that accepts any module name without validation.
///
/// This validator accepts any module name, including unknown ones.
#[derive(Debug, Clone, Copy)]
pub struct LenientRpcModuleValidator;
impl RpcModuleValidator for LenientRpcModuleValidator {
fn parse_selection(s: &str) -> Result<RpcModuleSelection, String> {
RpcModuleSelection::from_str(s).map_err(|e| format!("Failed to parse RPC modules: {}", e))
}
}
@@ -668,10 +784,12 @@ mod test {
assert!(result.is_ok());
assert_eq!(result.unwrap(), expected_selection);
// Test invalid selection should return error
// Test custom module selections now work (no longer return errors)
let result = RpcModuleSelection::from_str("invalid,unknown");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), ParseError::VariantNotFound);
assert!(result.is_ok());
let selection = result.unwrap();
assert!(selection.contains(&RethRpcModule::Other("invalid".to_string())));
assert!(selection.contains(&RethRpcModule::Other("unknown".to_string())));
// Test single valid selection: "eth"
let result = RpcModuleSelection::from_str("eth");
@@ -679,9 +797,160 @@ mod test {
let expected_selection = RpcModuleSelection::from([RethRpcModule::Eth]);
assert_eq!(result.unwrap(), expected_selection);
// Test single invalid selection: "unknown"
// Test single custom module selection: "unknown" now becomes Other
let result = RpcModuleSelection::from_str("unknown");
assert!(result.is_ok());
let expected_selection =
RpcModuleSelection::from([RethRpcModule::Other("unknown".to_string())]);
assert_eq!(result.unwrap(), expected_selection);
}
#[test]
fn test_rpc_module_other_variant() {
// Test parsing custom module
let custom_module = RethRpcModule::from_str("myCustomModule").unwrap();
assert_eq!(custom_module, RethRpcModule::Other("myCustomModule".to_string()));
// Test as_str for Other variant
assert_eq!(custom_module.as_str(), "myCustomModule");
// Test as_ref for Other variant
assert_eq!(custom_module.as_ref(), "myCustomModule");
// Test Display impl
assert_eq!(custom_module.to_string(), "myCustomModule");
}
#[test]
fn test_rpc_module_selection_with_mixed_modules() {
// Test selection with both standard and custom modules
let result = RpcModuleSelection::from_str("eth,admin,myCustomModule,anotherCustom");
assert!(result.is_ok());
let selection = result.unwrap();
assert!(selection.contains(&RethRpcModule::Eth));
assert!(selection.contains(&RethRpcModule::Admin));
assert!(selection.contains(&RethRpcModule::Other("myCustomModule".to_string())));
assert!(selection.contains(&RethRpcModule::Other("anotherCustom".to_string())));
}
#[test]
fn test_rpc_module_all_excludes_custom() {
// Test that All selection doesn't include custom modules
let all_selection = RpcModuleSelection::All;
// All should contain standard modules
assert!(all_selection.contains(&RethRpcModule::Eth));
assert!(all_selection.contains(&RethRpcModule::Admin));
// But All doesn't explicitly contain custom modules
// (though contains() returns true for all modules when selection is All)
assert_eq!(all_selection.len(), RethRpcModule::variant_count());
}
#[test]
fn test_rpc_module_equality_with_other() {
let other1 = RethRpcModule::Other("custom".to_string());
let other2 = RethRpcModule::Other("custom".to_string());
let other3 = RethRpcModule::Other("different".to_string());
assert_eq!(other1, other2);
assert_ne!(other1, other3);
assert_ne!(other1, RethRpcModule::Eth);
}
#[test]
fn test_rpc_module_is_other() {
// Standard modules should return false
assert!(!RethRpcModule::Eth.is_other());
assert!(!RethRpcModule::Admin.is_other());
assert!(!RethRpcModule::Debug.is_other());
// Other variants should return true
assert!(RethRpcModule::Other("custom".to_string()).is_other());
assert!(RethRpcModule::Other("mycustomrpc".to_string()).is_other());
}
#[test]
fn test_standard_variant_names_excludes_other() {
let standard_names: Vec<_> = RethRpcModule::standard_variant_names().collect();
// Verify "other" is not in the list
assert!(!standard_names.contains(&"other"));
// Should have exactly as many names as STANDARD_VARIANTS
assert_eq!(standard_names.len(), RethRpcModule::STANDARD_VARIANTS.len());
// Verify all standard variants have their names in the list
for variant in RethRpcModule::STANDARD_VARIANTS {
assert!(standard_names.contains(&variant.as_ref()));
}
}
#[test]
fn test_default_validator_accepts_standard_modules() {
// Should accept standard modules
let result = DefaultRpcModuleValidator::parse_selection("eth,admin,debug");
assert!(result.is_ok());
let selection = result.unwrap();
assert!(matches!(selection, RpcModuleSelection::Selection(_)));
}
#[test]
fn test_default_validator_rejects_unknown_modules() {
// Should reject unknown module names
let result = DefaultRpcModuleValidator::parse_selection("eth,mycustom");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), ParseError::VariantNotFound);
assert!(result.unwrap_err().contains("Unknown RPC module: 'mycustom'"));
let result = DefaultRpcModuleValidator::parse_selection("unknownmodule");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unknown RPC module: 'unknownmodule'"));
let result = DefaultRpcModuleValidator::parse_selection("eth,admin,xyz123");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unknown RPC module: 'xyz123'"));
}
#[test]
fn test_default_validator_all_selection() {
// Should accept "all" selection
let result = DefaultRpcModuleValidator::parse_selection("all");
assert!(result.is_ok());
assert_eq!(result.unwrap(), RpcModuleSelection::All);
}
#[test]
fn test_default_validator_none_selection() {
// Should accept "none" selection
let result = DefaultRpcModuleValidator::parse_selection("none");
assert!(result.is_ok());
assert_eq!(result.unwrap(), RpcModuleSelection::Selection(Default::default()));
}
#[test]
fn test_lenient_validator_accepts_unknown_modules() {
// Lenient validator should accept any module name without validation
let result = LenientRpcModuleValidator::parse_selection("eht,adimn,xyz123,customrpc");
assert!(result.is_ok());
let selection = result.unwrap();
if let RpcModuleSelection::Selection(modules) = selection {
assert!(modules.contains(&RethRpcModule::Other("eht".to_string())));
assert!(modules.contains(&RethRpcModule::Other("adimn".to_string())));
assert!(modules.contains(&RethRpcModule::Other("xyz123".to_string())));
assert!(modules.contains(&RethRpcModule::Other("customrpc".to_string())));
} else {
panic!("Expected Selection variant");
}
}
#[test]
fn test_default_validator_mixed_standard_and_custom() {
// Should reject mix of standard and custom modules
let result = DefaultRpcModuleValidator::parse_selection("eth,admin,mycustom,debug");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unknown RPC module: 'mycustom'"));
}
}