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)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Import JSON or CSV data, then open TUI (or save with --output)
|
/// Import JSON or CSV data, then open TUI (or save with --output)
|
||||||
Import {
|
Import(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>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Run a JSON command headless (repeatable)
|
/// Run a JSON command headless (repeatable)
|
||||||
Cmd {
|
Cmd(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
|
/// Run commands from a script file headless
|
||||||
Script {
|
Script(ScriptArgs),
|
||||||
/// Script file (one JSON command per line, # comments)
|
}
|
||||||
path: PathBuf,
|
|
||||||
|
|
||||||
/// Model file to load/save
|
#[derive(clap::Args)]
|
||||||
#[arg(short, long)]
|
struct ImportArgs {
|
||||||
file: Option<PathBuf>,
|
/// 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<()> {
|
fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
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 {
|
impl Runnable for ImportArgs {
|
||||||
None => {
|
fn run(self: Box<Self>, model_file: Option<PathBuf>) -> Result<()> {
|
||||||
let model = get_initial_model(&cli.file)?;
|
if self.files.is_empty() {
|
||||||
run_tui(model, cli.file, None)
|
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 {
|
let config = ImportConfig {
|
||||||
files,
|
categories: self.category,
|
||||||
category,
|
measures: self.measure,
|
||||||
measure,
|
time_fields: self.time,
|
||||||
time,
|
skip_fields: self.skip,
|
||||||
skip,
|
extractions: parse_colon_pairs(&self.extract),
|
||||||
extract,
|
axes: parse_colon_pairs(&self.axis),
|
||||||
axis,
|
formulas: self.formula,
|
||||||
formula,
|
name: self.name,
|
||||||
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 {
|
if self.no_wizard {
|
||||||
categories: category,
|
run_headless_import(import_value, &config, self.output, model_file)
|
||||||
measures: measure,
|
} else {
|
||||||
time_fields: time,
|
run_wizard_import(import_value, &config, model_file)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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