use clap::{Args, Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(name = "BEAM", version, about = "BEAM node runner")]
pub struct Cli {
#[arg(short = 'c', long = "config", value_name = "FILE")]
pub config: Option<String>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Start(StartArgs),
Migrate(MigrateArgs),
}
#[derive(Debug, Args)]
pub struct StartArgs {
#[arg(
long = "ws-server",
env = "WS_SERVER",
value_name = "BOOL",
default_value = "true"
)]
pub ws_server: String,
#[arg(
short = 'p',
long = "port",
env = "PORT",
value_name = "NUMBER",
default_value = "4944"
)]
pub port: u16,
#[arg(long = "cert-path", env = "CERT_PATH", value_name = "FILE")]
pub cert_path: Option<String>,
#[arg(long = "key-path", env = "KEY_PATH", value_name = "FILE")]
pub key_path: Option<String>,
#[arg(long = "peers", env = "PEERS", value_name = "URLS")]
pub peers: Option<String>,
#[arg(
long = "multicast",
env = "MULTICAST",
value_name = "BOOL",
default_value = "false"
)]
pub multicast: String,
#[arg(
long = "memory-storage",
env = "MEMORY_STORAGE",
value_name = "BOOL",
default_value = "false"
)]
pub memory_storage: String,
#[arg(
long = "redb-storage",
env = "REDB_STORAGE",
value_name = "BOOL",
default_value = "true"
)]
pub redb_storage: String,
#[arg(
long = "redb-path",
env = "REDB_PATH",
value_name = "PATH",
default_value = "beam.redb"
)]
pub redb_path: String,
#[arg(
long = "allow-public-space",
env = "ALLOW_PUBLIC_SPACE",
value_name = "BOOL",
default_value = "true"
)]
pub allow_public_space: String,
#[arg(
long = "shutdown-timeout",
env = "SHUTDOWN_TIMEOUT",
value_name = "SECONDS",
default_value = "30"
)]
pub shutdown_timeout: u64,
}
#[derive(Debug, Args)]
pub struct MigrateArgs {
#[arg(long = "from", value_name = "BACKEND")]
pub from: String,
#[arg(long = "to", value_name = "BACKEND")]
pub to: String,
#[arg(long = "source", value_name = "PATH")]
pub source: String,
#[arg(long = "target", value_name = "PATH")]
pub target: String,
#[arg(long = "batch-size", value_name = "N", default_value = "1000")]
pub batch_size: usize,
#[arg(long = "force")]
pub force: bool,
#[arg(long = "dry-run")]
pub dry_run: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn start_with_defaults() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::remove_var("PEERS");
std::env::remove_var("PORT");
}
let cli = Cli::parse_from(["beam", "start"]);
match cli.command {
Command::Start(args) => {
assert_eq!(args.port, 4944);
assert_eq!(args.ws_server, "true");
assert_eq!(args.multicast, "false");
assert_eq!(args.redb_storage, "true");
assert_eq!(args.redb_path, "beam.redb");
assert_eq!(args.memory_storage, "false");
assert_eq!(args.allow_public_space, "true");
assert!(args.cert_path.is_none());
assert!(args.key_path.is_none());
assert!(args.peers.is_none());
}
Command::Migrate(_) => panic!("expected Start"),
}
}
#[test]
fn start_with_custom_port() {
let cli = Cli::parse_from(["beam", "start", "--port", "8080"]);
match cli.command {
Command::Start(args) => assert_eq!(args.port, 8080),
_ => panic!("expected Start"),
}
}
#[test]
fn start_with_short_port_flag() {
let cli = Cli::parse_from(["beam", "start", "-p", "3000"]);
match cli.command {
Command::Start(args) => assert_eq!(args.port, 3000),
_ => panic!("expected Start"),
}
}
#[test]
fn start_with_peers() {
let cli = Cli::parse_from([
"beam",
"start",
"--peers",
"wss://peer1.example.com,wss://peer2.example.com",
]);
match cli.command {
Command::Start(args) => {
let peers = args.peers.unwrap();
assert!(peers.contains("peer1.example.com"));
assert!(peers.contains("peer2.example.com"));
}
_ => panic!("expected Start"),
}
}
#[test]
fn start_with_tls() {
let cli = Cli::parse_from([
"beam",
"start",
"--cert-path",
"/path/cert.pem",
"--key-path",
"/path/key.pem",
]);
match cli.command {
Command::Start(args) => {
assert_eq!(args.cert_path.unwrap(), "/path/cert.pem");
assert_eq!(args.key_path.unwrap(), "/path/key.pem");
}
_ => panic!("expected Start"),
}
}
#[test]
fn start_disable_redb() {
let cli = Cli::parse_from([
"beam",
"start",
"--redb-storage",
"false",
"--memory-storage",
"true",
]);
match cli.command {
Command::Start(args) => {
assert_eq!(args.redb_storage, "false");
assert_eq!(args.memory_storage, "true");
}
_ => panic!("expected Start"),
}
}
#[test]
fn start_disable_public_space() {
let cli = Cli::parse_from(["beam", "start", "--allow-public-space", "false"]);
match cli.command {
Command::Start(args) => assert_eq!(args.allow_public_space, "false"),
_ => panic!("expected Start"),
}
}
#[test]
fn start_with_custom_redb_path() {
let cli = Cli::parse_from(["beam", "start", "--redb-path", "/tmp/custom.beam"]);
match cli.command {
Command::Start(args) => assert_eq!(args.redb_path, "/tmp/custom.beam"),
_ => panic!("expected Start"),
}
}
#[test]
fn start_with_config_flag() {
let cli = Cli::parse_from(["beam", "-c", "config.toml", "start"]);
assert_eq!(cli.config, Some("config.toml".to_string()));
}
#[test]
fn start_with_config_long_flag() {
let cli = Cli::parse_from(["beam", "--config", "config.toml", "start"]);
assert_eq!(cli.config, Some("config.toml".to_string()));
}
#[test]
fn migrate_basic() {
let cli = Cli::parse_from([
"beam",
"migrate",
"--from",
"redb",
"--to",
"persy",
"--source",
"/tmp/source.redb",
"--target",
"/tmp/target.persy",
]);
match cli.command {
Command::Migrate(args) => {
assert_eq!(args.from, "redb");
assert_eq!(args.to, "persy");
assert_eq!(args.source, "/tmp/source.redb");
assert_eq!(args.target, "/tmp/target.persy");
assert_eq!(args.batch_size, 1000);
assert!(!args.force);
assert!(!args.dry_run);
}
_ => panic!("expected Migrate"),
}
}
#[test]
fn migrate_with_batch_size() {
let cli = Cli::parse_from([
"beam",
"migrate",
"--from",
"persy",
"--to",
"redb",
"--source",
"/tmp/s.persy",
"--target",
"/tmp/t.redb",
"--batch-size",
"500",
]);
match cli.command {
Command::Migrate(args) => assert_eq!(args.batch_size, 500),
_ => panic!("expected Migrate"),
}
}
#[test]
fn migrate_with_flags() {
let cli = Cli::parse_from([
"beam",
"migrate",
"--from",
"redb",
"--to",
"persy",
"--source",
"/tmp/s.redb",
"--target",
"/tmp/t.persy",
"--force",
"--dry-run",
]);
match cli.command {
Command::Migrate(args) => {
assert!(args.force);
assert!(args.dry_run);
}
_ => panic!("expected Migrate"),
}
}
#[test]
fn env_var_port() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("PORT", "9999");
}
let cli = Cli::parse_from(["beam", "start"]);
match cli.command {
Command::Start(args) => assert_eq!(args.port, 9999),
_ => panic!("expected Start"),
}
unsafe {
std::env::remove_var("PORT");
}
}
#[test]
fn env_var_peers() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("PEERS", "wss://env-peer.example.com");
}
let cli = Cli::parse_from(["beam", "start"]);
match cli.command {
Command::Start(args) => {
assert_eq!(args.peers.unwrap(), "wss://env-peer.example.com");
}
_ => panic!("expected Start"),
}
unsafe {
std::env::remove_var("PEERS");
}
}
#[test]
fn cli_flag_overrides_env_var() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("PORT", "9999");
}
let cli = Cli::parse_from(["beam", "start", "--port", "7777"]);
match cli.command {
Command::Start(args) => assert_eq!(args.port, 7777),
_ => panic!("expected Start"),
}
unsafe {
std::env::remove_var("PORT");
}
}
#[test]
fn migrate_missing_from() {
let result = Cli::try_parse_from([
"beam", "migrate", "--to", "persy", "--source", "s", "--target", "t",
]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("required"),
"error should mention required: {err}"
);
}
#[test]
fn migrate_missing_to() {
let result = Cli::try_parse_from([
"beam", "migrate", "--from", "redb", "--source", "s", "--target", "t",
]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("required"),
"error should mention required: {err}"
);
}
#[test]
fn migrate_missing_source() {
let result = Cli::try_parse_from([
"beam", "migrate", "--from", "redb", "--to", "persy", "--target", "t",
]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("required"),
"error should mention required: {err}"
);
}
#[test]
fn migrate_missing_target() {
let result = Cli::try_parse_from([
"beam", "migrate", "--from", "redb", "--to", "persy", "--source", "s",
]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("required"),
"error should mention required: {err}"
);
}
#[test]
fn no_subcommand_fails() {
let result = Cli::try_parse_from(["beam"]);
assert!(result.is_err(), "parsing without subcommand should fail");
}
}