diff --git a/src/main.rs b/src/main.rs index 642f343..3b4d75e 100644 --- a/src/main.rs +++ b/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, - - /// Mark field as category dimension (repeatable) - #[arg(long)] - category: Vec, - - /// Mark field as numeric measure (repeatable) - #[arg(long)] - measure: Vec, - - /// Mark field as time/date category (repeatable) - #[arg(long)] - time: Vec, - - /// Skip/exclude a field from import (repeatable) - #[arg(long)] - skip: Vec, - - /// Extract date component, e.g. "Date:Month" (repeatable) - #[arg(long)] - extract: Vec, - - /// Set category axis, e.g. "Payee:row" (repeatable) - #[arg(long)] - axis: Vec, - - /// Add formula, e.g. "Profit = Revenue - Cost" (repeatable) - #[arg(long)] - formula: Vec, - - /// Model name (default: "Imported Model") - #[arg(long)] - name: Option, - - /// Skip the interactive wizard - #[arg(long)] - no_wizard: bool, - - /// Save to file instead of opening TUI - #[arg(short, long)] - output: Option, - }, - + Import(ImportArgs), /// Run a JSON command headless (repeatable) - Cmd { - /// JSON command strings - json: Vec, - - /// Model file to load/save - #[arg(short, long)] - file: Option, - }, - + 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, - }, +#[derive(clap::Args)] +struct ImportArgs { + /// Files to import (multiple CSVs merge with a "File" category) + files: Vec, + + /// Mark field as category dimension (repeatable) + #[arg(long)] + category: Vec, + + /// Mark field as numeric measure (repeatable) + #[arg(long)] + measure: Vec, + + /// Mark field as time/date category (repeatable) + #[arg(long)] + time: Vec, + + /// Skip/exclude a field from import (repeatable) + #[arg(long)] + skip: Vec, + + /// Extract date component, e.g. "Date:Month" (repeatable) + #[arg(long)] + extract: Vec, + + /// Set category axis, e.g. "Payee:row" (repeatable) + #[arg(long)] + axis: Vec, + + /// Add formula, e.g. "Profit = Revenue - Cost" (repeatable) + #[arg(long)] + formula: Vec, + + /// Model name (default: "Imported Model") + #[arg(long)] + name: Option, + + /// Skip the interactive wizard + #[arg(long)] + no_wizard: bool, + + /// Save to file instead of opening TUI + #[arg(short, long)] + output: Option, +} + +#[derive(clap::Args)] +struct CmdArgs { + /// JSON command strings + json: Vec, + + /// Model file to load/save + #[arg(short, long)] + file: Option, +} + +#[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, +} + +trait Runnable { + fn run(self: Box, model_file: Option) -> Result<()>; +} + +impl From for Box { + 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, model_file: Option) -> Result<()> { + let model = get_initial_model(&model_file)?; + run_tui(model, model_file, None) + } } fn main() -> Result<()> { let cli = Cli::parse(); + let cmd: Box = cli + .command + .map(|c| -> Box { 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, model_file: Option) -> 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, _model_file: Option) -> 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, _model_file: Option) -> Result<()> { + run_headless_script(&self.path, &self.file) } }