1use std::{fs::File, io::BufReader};
2use calamine::{Reader, Sheets};
3use heck::ToSnakeCase;
4use indexmap::IndexMap;
5use serde::Serialize;
6use serde_json::{json, Value};
7
8use crate::{OptionSet, PathData, ReadMode};
9
10
11#[derive(Debug, Clone)]
13pub struct WorkbookInfo {
14 pub filename: String,
15 pub extension: String,
16 pub selected: Option<Vec<String>>,
17 pub sheets: Vec<String>,
18}
19
20impl WorkbookInfo {
21 pub fn new(path_data: &PathData, selected: &[String], sheet_refs: &[String]) -> Self {
22 WorkbookInfo {
23 extension: path_data.extension(),
24 filename: path_data.filename(),
25 selected: Some(selected.to_vec()),
26 sheets: sheet_refs.to_vec(),
27 }
28 }
29
30 pub fn simple(path_data: &PathData) -> Self {
31 let sheet_name = "single";
32 WorkbookInfo {
33 extension: path_data.extension(),
34 filename: path_data.filename(),
35 selected: None,
36 sheets: vec![sheet_name.to_owned()],
37 }
38 }
39
40 pub fn ext(&self) -> String {
41 self.extension.to_owned()
42 }
43
44 pub fn name(&self) -> String {
45 self.filename.to_owned()
46 }
47
48 pub fn sheet(&self, index: usize) -> (String, usize) {
49 let sheet_name = self.sheets.get(index).unwrap_or(&"single".to_owned()).to_owned();
50 (sheet_name, index)
51 }
52
53 pub fn sheets(&self) -> Vec<String> {
54 self.sheets.clone()
55 }
56}
57
58
59#[derive(Debug, Clone)]
61pub struct ResultSet {
62 pub filename: String,
63 pub extension: String,
64 pub selected: Option<Vec<String>>,
65 pub sheets: Vec<String>,
66 pub keys: Vec<String>,
67 pub num_rows: usize,
68 pub data: SpreadData,
69 pub out_ref: Option<String>,
70 pub opts: OptionSet,
71 pub header_row_index: Option<usize>,
77 pub body_start_index: usize,
81}
82
83impl ResultSet {
84
85 pub fn new(info: &WorkbookInfo, keys: &[String], data_set: DataSet, opts: &OptionSet, out_ref: Option<&str>, header_row_index: Option<usize>, body_start_index: usize) -> Self {
90 let (num_rows, data) = match data_set {
91 DataSet::WithRows(size, rows) => (size, rows),
92 DataSet::Count(size) => (size, vec![])
93 };
94 ResultSet {
95 extension: info.ext(),
96 filename: info.name(),
97 selected: info.selected.clone(),
98 sheets: info.sheets(),
99 keys: keys.to_vec(),
100 num_rows,
101 data: SpreadData::from_single(data),
102 out_ref: out_ref.map(|s| s.to_string()),
103 opts: opts.to_owned(),
104 header_row_index,
105 body_start_index,
106 }
107 }
108
109 pub fn from_multiple(sheets: &[SheetDataSet], info: &WorkbookInfo, opts: &OptionSet) -> Self {
110 let selected = None;
111 let mut sheet_names = vec![];
112 let filename = info.filename.clone();
113 let extension = info.extension.clone();
114 let mut keys: Vec<String> = vec![];
115 let mut num_rows = 0;
116 for (sheet_index, sheet) in sheets.iter().enumerate() {
117 num_rows += sheet.num_rows;
118 sheet_names.push(sheet.name());
119 if sheet_index == 0 {
120 keys = sheet.keys.clone();
121 }
122 }
123 ResultSet {
124 extension,
125 filename,
126 selected,
127 sheets: sheet_names,
128 keys,
129 num_rows,
130 data: SpreadData::Multiple(sheets.to_vec()),
131 out_ref: None,
132 opts: opts.to_owned(),
133 header_row_index: None,
137 body_start_index: 0,
138 }
139 }
140
141
142 pub fn multimode(&self) -> bool {
143 matches!(self.data, SpreadData::Multiple(_))
144 }
145
146 pub fn to_json(&self) -> Value {
148 let mut result = json!({
149 "name": self.filename,
150 "extension": self.extension,
151 "selected": self.selected.clone().unwrap_or(vec![]),
152 "sheets": self.sheets,
153 "num_rows": self.num_rows,
154 "fields": self.keys,
155 "multimode": self.multimode(),
156 "data": self.data.to_json(),
157 "opts": self.opts.to_json()
158 });
159 if let Some(out_ref_str) = self.out_ref.clone() {
160 result["outref"] = json!(out_ref_str);
161 }
162 result
163 }
164
165 pub fn to_output_lines(&self, json_lines: bool) -> Vec<String> {
167 let selected_names = self.selected.clone().unwrap_or(vec![]);
168 let num_selected = selected_names.len();
169 let plural = if num_selected > 1 {
170 "s"
171 } else {
172 ""
173 };
174 let mut lines = vec![
175 format!("name:{}", self.filename),
176 format!("extension: {}", self.extension),
177
178 format!("sheets: {}", self.sheets.join(", ")),
179 ];
180 if num_selected > 0 {
181 lines.push(format!("selected sheet{}: {}", plural, selected_names.join(", ")));
182 }
183 lines.push(format!("row count: {}", self.num_rows));
184 lines.push(format!("fields: {}", self.keys.join(",")));
185 lines.push(format!("multimode: {}", self.multimode()));
186 for opt_line in self.opts.to_lines() {
187 lines.push(opt_line);
188 }
189 if let Some(out_ref_str) = self.out_ref.clone() {
190 lines.push(format!("output reference: {}", out_ref_str));
191 } else {
192 let has_many_sheets = self.sheets.len() > 1;
193 if !has_many_sheets || !self.multimode() {
194 lines.push("data:".to_owned());
195 }
196 if json_lines {
197 for sheet in &self.data.sheets() {
198 if has_many_sheets {
199 lines.push(format!("Sheet `{}` ({}):", sheet.name(), sheet.num_rows));
200 }
201 for item in &sheet.rows {
202 lines.push(format!("{}", json!(item)));
203 }
204 }
205 } else {
206 if self.multimode() {
207 for sheet in self.data.sheets() {
208 lines.push(format!("Sheet `{}` ({}):", sheet.name(), sheet.num_rows));
209 lines.push(format!("{}", json!(sheet)));
210 }
211 } else {
212 lines.push(format!("{}", self.data.to_json()));
213 }
214 }
215 }
216 lines
217 }
218
219 pub fn to_vec(&self) -> Vec<IndexMap<String, Value>> {
222 self.data.first_sheet().clone()
223 }
224
225 pub fn json_data(&self) -> Value {
227 json!(self.data)
228 }
229
230 pub fn rows(&self) -> Vec<String> {
232 let sheet = self.data.first_sheet();
233 let mut lines = Vec::with_capacity(sheet.len());
234 for row in &sheet {
235 lines.push(json!(row).to_string());
236 }
237 lines
238 }
239
240}
241
242#[derive(Debug, Clone, Serialize)]
243pub struct SheetDataSet {
244 pub sheet: (String, String),
245 pub num_rows: usize,
246 pub keys: Vec<String>,
247 pub rows: Vec<IndexMap<String, Value>>
248}
249
250impl SheetDataSet {
251
252
253
254 pub fn new(name: &str, keys: &[String], rows: &[IndexMap<String, Value>], total: usize) -> Self {
255 Self {
256 sheet: (name.to_string(), name.to_snake_case()),
257 keys: keys.to_vec(),
258 rows: rows.to_vec(),
259 num_rows: total
260 }
261 }
262
263 pub fn key(&self) -> String {
264 self.sheet.1.clone()
265 }
266
267 pub fn name(&self) -> String {
268 self.sheet.0.clone()
269 }
270}
271
272#[derive(Debug, Clone, Serialize)]
273pub enum SpreadData {
274 Single(Vec<IndexMap<String, Value>>),
275 Multiple(Vec<SheetDataSet>)
276}
277
278impl SpreadData {
279 pub fn from_single(rows: Vec<IndexMap<String, Value>>) -> Self {
280 SpreadData::Single(rows)
281 }
282
283 pub fn from_multiple(sheet_data: &[SheetDataSet]) -> Self {
284 SpreadData::Multiple(sheet_data.to_owned())
285 }
286
287 pub fn first_sheet(&self) -> Vec<IndexMap<String, Value>> {
288 match self {
289 SpreadData::Single(rows) => rows.to_owned(),
290 SpreadData::Multiple(sheets) => {
291 if let Some(sheet) = sheets.first() {
292 sheet.rows.to_owned()
293 } else {
294 vec![]
295 }
296 }
297 }
298 }
299
300 pub fn sheets(&self) -> Vec<SheetDataSet> {
302 match self {
303 SpreadData::Single(_) => vec![],
304 SpreadData::Multiple(sheets) => sheets.to_owned()
305 }
306 }
307
308 pub fn to_json(&self) -> Value {
309 match self {
310 SpreadData::Single(sheet) => json!(sheet),
311 SpreadData::Multiple(sheet_map) => json!(sheet_map)
312 }
313 }
314}
315
316
317#[derive(Debug, Clone, Serialize)]
318pub enum DataSet {
319 WithRows(usize, Vec<IndexMap<String, Value>>),
320 Count(usize)
321}
322
323impl DataSet {
324 pub fn from_count_and_rows(count: usize, rows: Vec<IndexMap<String, Value>>, opts: &OptionSet) -> Self {
325 match opts.read_mode() {
326 ReadMode::Sync | ReadMode::PreviewMultiple => DataSet::WithRows(count, rows),
327 ReadMode::Async => DataSet::Count(count),
328 }
329 }
330}
331
332
333pub fn to_index_map(row: &[serde_json::Value], headers: &[String]) -> IndexMap<String, Value> {
334 let mut hm: IndexMap<String, serde_json::Value> = IndexMap::new();
335 for (sub_index, hk) in headers.iter().enumerate() {
336 if let Some(cell) = row.get(sub_index) {
337 hm.insert(hk.to_owned(), cell.to_owned());
338 }
339 }
340 hm
341}
342
343pub fn match_sheet_name_and_index(workbook: &mut Sheets<BufReader<File>>, opts: &OptionSet) -> (Vec<String>, Vec<String>, Vec<usize>) {
344 let mut sheet_indices = vec![];
345 let mut selected_names: Vec<String> = vec![];
346 let sheet_names = workbook.worksheets().into_iter().map(|ws| ws.0).collect::<Vec<String>>();
347 if let Some(sheet_keys) = opts.selected.clone() {
348 for sheet_key in sheet_keys {
349 if let Some(sheet_index) = sheet_names.iter().position(|s| s.to_snake_case() == sheet_key.to_snake_case()) {
350 sheet_indices.push(sheet_index);
351 selected_names.push(sheet_names[sheet_index].clone());
352 }
353 }
354 }
355 if sheet_indices.is_empty() && !opts.indices.is_empty() {
356 for s_index in opts.indices.clone() {
357 let sheet_index = s_index as usize;
358 if let Some(sheet_name) = sheet_names.get(sheet_index) {
359 sheet_indices.push(sheet_index);
360 selected_names.push(sheet_name.to_owned());
361 }
362 }
363 }
364 if sheet_indices.is_empty() {
365 sheet_indices = vec![0];
366 if !sheet_names.is_empty() {
367 selected_names.push(sheet_names[0].clone());
368 }
369 }
370 (selected_names, sheet_names, sheet_indices)
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use calamine::open_workbook_auto;
377
378 const SAMPLE_PATH: &str = "data/sample-data-2.ods";
380
381 fn opts_selecting(sheet_key: &str) -> OptionSet {
382 OptionSet::new(SAMPLE_PATH).sheet_name(sheet_key)
383 }
384
385 #[test]
386 fn test_sheet_name_matching_is_case_insensitive_and_ignores_punctuation() {
387 let mut workbook = open_workbook_auto(SAMPLE_PATH).unwrap();
388 for variant in ["results 1", "RESULTS 1", "Results_1", "results-1", " results 1 "] {
391 let opts = opts_selecting(variant);
392 let (selected_names, _sheet_names, sheet_indices) = match_sheet_name_and_index(&mut workbook, &opts);
393 assert_eq!(selected_names, vec!["results 1".to_string()], "variant '{}' should match 'results 1'", variant);
394 assert_eq!(sheet_indices, vec![1], "variant '{}' should resolve to index 1", variant);
395 }
396 }
397
398 #[test]
399 fn test_sheet_name_matching_handles_names_with_no_spaces() {
400 let mut workbook = open_workbook_auto(SAMPLE_PATH).unwrap();
403 for variant in ["Rsults-2", "rsults-2", "RSULTS-2", "rsults_2"] {
404 let opts = opts_selecting(variant);
405 let (selected_names, _sheet_names, sheet_indices) = match_sheet_name_and_index(&mut workbook, &opts);
406 assert_eq!(selected_names, vec!["Rsults-2".to_string()], "variant '{}' should match 'Rsults-2'", variant);
407 assert_eq!(sheet_indices, vec![0]);
408 }
409 }
410
411 #[test]
412 fn test_sheet_name_matching_falls_back_to_first_sheet_when_unmatched() {
413 let mut workbook = open_workbook_auto(SAMPLE_PATH).unwrap();
414 let opts = opts_selecting("nonexistent sheet name");
415 let (selected_names, sheet_names, sheet_indices) = match_sheet_name_and_index(&mut workbook, &opts);
416 assert_eq!(sheet_names, vec!["Rsults-2".to_string(), "results 1".to_string()]);
417 assert_eq!(selected_names, vec!["Rsults-2".to_string()]);
419 assert_eq!(sheet_indices, vec![0]);
420 }
421
422 #[test]
423 fn test_sheet_index_selection_still_works() {
424 let mut workbook = open_workbook_auto(SAMPLE_PATH).unwrap();
425 let opts = OptionSet::new(SAMPLE_PATH).sheet_index(1);
426 let (selected_names, _sheet_names, sheet_indices) = match_sheet_name_and_index(&mut workbook, &opts);
427 assert_eq!(selected_names, vec!["results 1".to_string()]);
428 assert_eq!(sheet_indices, vec![1]);
429 }
430}
431