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:
228
src/main.rs
228
src/main.rs
@ -32,122 +32,146 @@ struct Cli {
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Import JSON or CSV data, then open TUI (or save with --output)
|
||||
Import {
|
||||
/// Files to import (multiple CSVs merge with a "File" category)
|
||||
files: Vec<PathBuf>,
|
||||
|
||||
/// Mark field as category dimension (repeatable)
|
||||
#[arg(long)]
|
||||
category: Vec<String>,
|
||||
|
||||
/// Mark field as numeric measure (repeatable)
|
||||
#[arg(long)]
|
||||
measure: Vec<String>,
|
||||
|
||||
/// Mark field as time/date category (repeatable)
|
||||
#[arg(long)]
|
||||
time: Vec<String>,
|
||||
|
||||
/// Skip/exclude a field from import (repeatable)
|
||||
#[arg(long)]
|
||||
skip: Vec<String>,
|
||||
|
||||
/// Extract date component, e.g. "Date:Month" (repeatable)
|
||||
#[arg(long)]
|
||||
extract: Vec<String>,
|
||||
|
||||
/// Set category axis, e.g. "Payee:row" (repeatable)
|
||||
#[arg(long)]
|
||||
axis: Vec<String>,
|
||||
|
||||
/// Add formula, e.g. "Profit = Revenue - Cost" (repeatable)
|
||||
#[arg(long)]
|
||||
formula: Vec<String>,
|
||||
|
||||
/// Model name (default: "Imported Model")
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
|
||||
/// Skip the interactive wizard
|
||||
#[arg(long)]
|
||||
no_wizard: bool,
|
||||
|
||||
/// Save to file instead of opening TUI
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
},
|
||||
|
||||
Import(ImportArgs),
|
||||
/// Run a JSON command headless (repeatable)
|
||||
Cmd {
|
||||
/// JSON command strings
|
||||
json: Vec<String>,
|
||||
|
||||
/// Model file to load/save
|
||||
#[arg(short, long)]
|
||||
file: Option<PathBuf>,
|
||||
},
|
||||
|
||||
Cmd(CmdArgs),
|
||||
/// Run commands from a script file headless
|
||||
Script {
|
||||
/// Script file (one JSON command per line, # comments)
|
||||
path: PathBuf,
|
||||
Script(ScriptArgs),
|
||||
}
|
||||
|
||||
/// Model file to load/save
|
||||
#[arg(short, long)]
|
||||
file: Option<PathBuf>,
|
||||
},
|
||||
#[derive(clap::Args)]
|
||||
struct ImportArgs {
|
||||
/// Files to import (multiple CSVs merge with a "File" category)
|
||||
files: Vec<PathBuf>,
|
||||
|
||||
/// Mark field as category dimension (repeatable)
|
||||
#[arg(long)]
|
||||
category: Vec<String>,
|
||||
|
||||
/// Mark field as numeric measure (repeatable)
|
||||
#[arg(long)]
|
||||
measure: Vec<String>,
|
||||
|
||||
/// Mark field as time/date category (repeatable)
|
||||
#[arg(long)]
|
||||
time: Vec<String>,
|
||||
|
||||
/// Skip/exclude a field from import (repeatable)
|
||||
#[arg(long)]
|
||||
skip: Vec<String>,
|
||||
|
||||
/// Extract date component, e.g. "Date:Month" (repeatable)
|
||||
#[arg(long)]
|
||||
extract: Vec<String>,
|
||||
|
||||
/// Set category axis, e.g. "Payee:row" (repeatable)
|
||||
#[arg(long)]
|
||||
axis: Vec<String>,
|
||||
|
||||
/// Add formula, e.g. "Profit = Revenue - Cost" (repeatable)
|
||||
#[arg(long)]
|
||||
formula: Vec<String>,
|
||||
|
||||
/// Model name (default: "Imported Model")
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
|
||||
/// Skip the interactive wizard
|
||||
#[arg(long)]
|
||||
no_wizard: bool,
|
||||
|
||||
/// Save to file instead of opening TUI
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
struct CmdArgs {
|
||||
/// JSON command strings
|
||||
json: Vec<String>,
|
||||
|
||||
/// Model file to load/save
|
||||
#[arg(short, long)]
|
||||
file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[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();
|
||||
let cmd: Box<dyn Runnable> = cli
|
||||
.command
|
||||
.map(|c| -> Box<dyn Runnable> { c.into() })
|
||||
.unwrap_or_else(|| Box::new(OpenTui));
|
||||
cmd.run(cli.file)
|
||||
}
|
||||
|
||||
match cli.command {
|
||||
None => {
|
||||
let model = get_initial_model(&cli.file)?;
|
||||
run_tui(model, cli.file, None)
|
||||
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");
|
||||
}
|
||||
let import_value = get_import_data(&self.files)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to parse import files"))?;
|
||||
|
||||
Some(Commands::Import {
|
||||
files,
|
||||
category,
|
||||
measure,
|
||||
time,
|
||||
skip,
|
||||
extract,
|
||||
axis,
|
||||
formula,
|
||||
name,
|
||||
no_wizard,
|
||||
output,
|
||||
}) => {
|
||||
let import_value = if 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 config = ImportConfig {
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if no_wizard {
|
||||
run_headless_import(import_value, &config, output, cli.file)
|
||||
} else {
|
||||
run_wizard_import(import_value, &config, cli.file)
|
||||
}
|
||||
if self.no_wizard {
|
||||
run_headless_import(import_value, &config, self.output, model_file)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user