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}