Skip to main content

spreadsheet_mcp/cli/
mod.rs

1pub mod commands;
2pub mod errors;
3pub mod output;
4
5use anyhow::Result;
6use clap::{Parser, Subcommand, ValueEnum};
7use serde_json::Value;
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Copy, ValueEnum)]
11pub enum OutputFormat {
12    Json,
13    Csv,
14}
15
16#[derive(Debug, Clone, Copy, ValueEnum)]
17pub enum TableReadFormat {
18    Json,
19    Values,
20    Csv,
21}
22
23#[derive(Debug, Clone, Copy, ValueEnum)]
24pub enum FindValueMode {
25    Value,
26    Label,
27}
28
29#[derive(Debug, Clone, Copy, ValueEnum)]
30pub enum FormulaSort {
31    Complexity,
32    Count,
33}
34
35#[derive(Debug, Clone, Copy, ValueEnum)]
36pub enum TraceDirectionArg {
37    Precedents,
38    Dependents,
39}
40
41#[derive(Debug, Parser)]
42#[command(
43    name = "spreadsheet-cli",
44    version,
45    about = "Spreadsheet command line interface"
46)]
47pub struct Cli {
48    #[arg(long, value_enum, default_value_t = OutputFormat::Json, global = true)]
49    pub format: OutputFormat,
50
51    #[arg(long, global = true)]
52    pub compact: bool,
53
54    #[arg(long, global = true)]
55    pub quiet: bool,
56
57    #[command(subcommand)]
58    pub command: Commands,
59}
60
61#[derive(Debug, Subcommand)]
62pub enum Commands {
63    ListSheets {
64        file: PathBuf,
65    },
66    SheetOverview {
67        file: PathBuf,
68        sheet: String,
69    },
70    RangeValues {
71        file: PathBuf,
72        sheet: String,
73        ranges: Vec<String>,
74    },
75    ReadTable {
76        file: PathBuf,
77        #[arg(long)]
78        sheet: Option<String>,
79        #[arg(long)]
80        range: Option<String>,
81        #[arg(long = "table-format", value_enum)]
82        table_format: Option<TableReadFormat>,
83    },
84    FindValue {
85        file: PathBuf,
86        query: String,
87        #[arg(long)]
88        sheet: Option<String>,
89        #[arg(long, value_enum)]
90        mode: Option<FindValueMode>,
91    },
92    FormulaMap {
93        file: PathBuf,
94        sheet: String,
95        #[arg(long)]
96        limit: Option<u32>,
97        #[arg(long, value_enum)]
98        sort_by: Option<FormulaSort>,
99    },
100    FormulaTrace {
101        file: PathBuf,
102        sheet: String,
103        cell: String,
104        direction: TraceDirectionArg,
105    },
106    Describe {
107        file: PathBuf,
108    },
109    TableProfile {
110        file: PathBuf,
111        #[arg(long)]
112        sheet: Option<String>,
113    },
114    Copy {
115        source: PathBuf,
116        dest: PathBuf,
117    },
118    Edit {
119        file: PathBuf,
120        sheet: String,
121        edits: Vec<String>,
122    },
123    Recalculate {
124        file: PathBuf,
125    },
126    Diff {
127        original: PathBuf,
128        modified: PathBuf,
129    },
130}
131
132pub async fn run_command(command: Commands) -> Result<Value> {
133    match command {
134        Commands::ListSheets { file } => commands::read::list_sheets(file).await,
135        Commands::SheetOverview { file, sheet } => {
136            commands::read::sheet_overview(file, sheet).await
137        }
138        Commands::RangeValues {
139            file,
140            sheet,
141            ranges,
142        } => commands::read::range_values(file, sheet, ranges).await,
143        Commands::ReadTable {
144            file,
145            sheet,
146            range,
147            table_format,
148        } => commands::read::read_table(file, sheet, range, table_format).await,
149        Commands::FindValue {
150            file,
151            query,
152            sheet,
153            mode,
154        } => commands::read::find_value(file, query, sheet, mode).await,
155        Commands::FormulaMap {
156            file,
157            sheet,
158            limit,
159            sort_by,
160        } => commands::read::formula_map(file, sheet, limit, sort_by).await,
161        Commands::FormulaTrace {
162            file,
163            sheet,
164            cell,
165            direction,
166        } => commands::read::formula_trace(file, sheet, cell, direction).await,
167        Commands::Describe { file } => commands::read::describe(file).await,
168        Commands::TableProfile { file, sheet } => commands::read::table_profile(file, sheet).await,
169        Commands::Copy { source, dest } => commands::write::copy(source, dest).await,
170        Commands::Edit { file, sheet, edits } => commands::write::edit(file, sheet, edits).await,
171        Commands::Recalculate { file } => commands::recalc::recalculate(file).await,
172        Commands::Diff { original, modified } => commands::diff::diff(original, modified).await,
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn parses_global_flags_and_read_table() {
182        let cli = Cli::try_parse_from([
183            "spreadsheet-cli",
184            "--format",
185            "json",
186            "--compact",
187            "--quiet",
188            "read-table",
189            "workbook.xlsx",
190            "--sheet",
191            "Sheet1",
192            "--range",
193            "A1:B10",
194            "--table-format",
195            "values",
196        ])
197        .expect("parse command");
198
199        assert!(cli.compact);
200        assert!(cli.quiet);
201        match cli.command {
202            Commands::ReadTable {
203                file,
204                sheet,
205                range,
206                table_format,
207            } => {
208                assert_eq!(file, PathBuf::from("workbook.xlsx"));
209                assert_eq!(sheet.as_deref(), Some("Sheet1"));
210                assert_eq!(range.as_deref(), Some("A1:B10"));
211                assert!(matches!(table_format, Some(TableReadFormat::Values)));
212            }
213            other => panic!("unexpected command: {other:?}"),
214        }
215    }
216
217    #[test]
218    fn parses_formula_trace_direction() {
219        let cli = Cli::try_parse_from([
220            "spreadsheet-cli",
221            "formula-trace",
222            "workbook.xlsx",
223            "Sheet1",
224            "C3",
225            "dependents",
226        ])
227        .expect("parse command");
228
229        match cli.command {
230            Commands::FormulaTrace {
231                direction,
232                cell,
233                sheet,
234                ..
235            } => {
236                assert_eq!(cell, "C3");
237                assert_eq!(sheet, "Sheet1");
238                assert!(matches!(direction, TraceDirectionArg::Dependents));
239            }
240            other => panic!("unexpected command: {other:?}"),
241        }
242    }
243}