refactor(cli): refactor command structure and introduce Runnable trait

Refactor the CLI command structure by moving subcommand arguments into
dedicated structs (ImportArgs, CmdArgs, and ScriptArgs).

Introduce a Runnable trait to allow for polymorphic command execution,
replacing the large match statement in the main function with a more
scalable approach.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
Edward Langley
2026-04-08 09:44:58 -07:00
parent 3885fc19c8
commit a3a74d2787

View File

@ -32,7 +32,15 @@ struct Cli {
#[derive(Subcommand)]
enum Commands {
/// Import JSON or CSV data, then open TUI (or save with --output)
Import {
Import(ImportArgs),
/// Run a JSON command headless (repeatable)
Cmd(CmdArgs),
/// Run commands from a script file headless
Script(ScriptArgs),
}
#[derive(clap::Args)]
struct ImportArgs {
/// Files to import (multiple CSVs merge with a "File" category)
files: Vec<PathBuf>,
@ -75,79 +83,95 @@ enum Commands {
/// Save to file instead of opening TUI
#[arg(short, long)]
output: Option<PathBuf>,
},
}
/// Run a JSON command headless (repeatable)
Cmd {
#[derive(clap::Args)]
struct CmdArgs {
/// JSON command strings
json: Vec<String>,
/// Model file to load/save
#[arg(short, long)]
file: Option<PathBuf>,
},
}
/// Run commands from a script file headless
Script {
#[derive(clap::Args)]
struct ScriptArgs {
/// Script file (one JSON command per line, # comments)
path: PathBuf,
/// Model file to load/save
#[arg(short, long)]
file: Option<PathBuf>,
},
}
trait Runnable {
fn run(self: Box<Self>, model_file: Option<PathBuf>) -> Result<()>;
}
impl From<Commands> for Box<dyn Runnable> {
fn from(cmd: Commands) -> Self {
match cmd {
Commands::Import(args) => Box::new(args),
Commands::Cmd(args) => Box::new(args),
Commands::Script(args) => Box::new(args),
}
}
}
struct OpenTui;
impl Runnable for OpenTui {
fn run(self: Box<Self>, model_file: Option<PathBuf>) -> Result<()> {
let model = get_initial_model(&model_file)?;
run_tui(model, model_file, None)
}
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
None => {
let model = get_initial_model(&cli.file)?;
run_tui(model, cli.file, None)
let cmd: Box<dyn Runnable> = cli
.command
.map(|c| -> Box<dyn Runnable> { c.into() })
.unwrap_or_else(|| Box::new(OpenTui));
cmd.run(cli.file)
}
Some(Commands::Import {
files,
category,
measure,
time,
skip,
extract,
axis,
formula,
name,
no_wizard,
output,
}) => {
let import_value = if files.is_empty() {
impl Runnable for ImportArgs {
fn run(self: Box<Self>, model_file: Option<PathBuf>) -> Result<()> {
if self.files.is_empty() {
anyhow::bail!("No files specified for import");
} else {
get_import_data(&files)
.ok_or_else(|| anyhow::anyhow!("Failed to parse import files"))?
};
}
let import_value = get_import_data(&self.files)
.ok_or_else(|| anyhow::anyhow!("Failed to parse import files"))?;
let config = ImportConfig {
categories: category,
measures: measure,
time_fields: time,
skip_fields: skip,
extractions: parse_colon_pairs(&extract),
axes: parse_colon_pairs(&axis),
formulas: formula,
name,
categories: self.category,
measures: self.measure,
time_fields: self.time,
skip_fields: self.skip,
extractions: parse_colon_pairs(&self.extract),
axes: parse_colon_pairs(&self.axis),
formulas: self.formula,
name: self.name,
};
if no_wizard {
run_headless_import(import_value, &config, output, cli.file)
if self.no_wizard {
run_headless_import(import_value, &config, self.output, model_file)
} else {
run_wizard_import(import_value, &config, cli.file)
run_wizard_import(import_value, &config, model_file)
}
}
}
Some(Commands::Cmd { json, file }) => run_headless_commands(&json, &file),
impl Runnable for CmdArgs {
fn run(self: Box<Self>, _model_file: Option<PathBuf>) -> Result<()> {
run_headless_commands(&self.json, &self.file)
}
}
Some(Commands::Script { path, file }) => run_headless_script(&path, &file),
impl Runnable for ScriptArgs {
fn run(self: Box<Self>, _model_file: Option<PathBuf>) -> Result<()> {
run_headless_script(&self.path, &self.file)
}
}