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::key_segment::insert_key_segment;
9use crate::{Column, OptionSet, PathData, ReadMode};
10
11
12#[derive(Debug, Clone)]
14pub struct WorkbookInfo {
15 pub filename: String,
16 pub extension: String,
17 pub selected: Option<Vec<String>>,
18 pub sheets: Vec<String>,
19}
20
21impl WorkbookInfo {
22 pub fn new(path_data: &PathData, selected: &[String], sheet_refs: &[String]) -> Self {
23 WorkbookInfo {
24 extension: path_data.extension(),
25 filename: path_data.filename(),
26 selected: Some(selected.to_vec()),
27 sheets: sheet_refs.to_vec(),
28 }
29 }
30
31 pub fn simple(path_data: &PathData) -> Self {
32 let sheet_name = "single";
33 WorkbookInfo {
34 extension: path_data.extension(),
35 filename: path_data.filename(),
36 selected: None,
37 sheets: vec![sheet_name.to_owned()],
38 }
39 }
40
41 pub fn ext(&self) -> String {
42 self.extension.to_owned()
43 }
44
45 pub fn name(&self) -> String {
46 self.filename.to_owned()
47 }
48
49 pub fn sheet(&self, index: usize) -> (String, usize) {
50 let sheet_name = self.sheets.get(index).unwrap_or(&"single".to_owned()).to_owned();
51 (sheet_name, index)
52 }
53
54 pub fn sheets(&self) -> Vec<String> {
55 self.sheets.clone()
56 }
57}
58
59
60#[derive(Debug, Clone)]
62pub struct ResultSet {
63 pub filename: String,
64 pub extension: String,
65 pub selected: Option<Vec<String>>,
66 pub sheets: Vec<String>,
67 pub keys: Vec<String>,
68 pub num_rows: usize,
69 pub data: SpreadData,
70 pub out_ref: Option<String>,
71 pub opts: OptionSet,
72 pub header_row_index: Option<usize>,
78 pub body_start_index: usize,
82}
83
84impl ResultSet {
85
86 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 {
91 let (num_rows, data) = match data_set {
92 DataSet::WithRows(size, rows) => (size, rows),
93 DataSet::Count(size) => (size, vec![])
94 };
95 ResultSet {
96 extension: info.ext(),
97 filename: info.name(),
98 selected: info.selected.clone(),
99 sheets: info.sheets(),
100 keys: keys.to_vec(),
101 num_rows,
102 data: SpreadData::from_single(data),
103 out_ref: out_ref.map(|s| s.to_string()),
104 opts: opts.to_owned(),
105 header_row_index,
106 body_start_index,
107 }
108 }
109
110 pub fn from_multiple(sheets: &[SheetDataSet], info: &WorkbookInfo, opts: &OptionSet) -> Self {
111 let selected = None;
112 let mut sheet_names = vec![];
113 let filename = info.filename.clone();
114 let extension = info.extension.clone();
115 let mut keys: Vec<String> = vec![];
116 let mut num_rows = 0;
117 for (sheet_index, sheet) in sheets.iter().enumerate() {
118 num_rows += sheet.num_rows;
119 sheet_names.push(sheet.name());
120 if sheet_index == 0 {
121 keys = sheet.keys.clone();
122 }
123 }
124 ResultSet {
125 extension,
126 filename,
127 selected,
128 sheets: sheet_names,
129 keys,
130 num_rows,
131 data: SpreadData::Multiple(sheets.to_vec()),
132 out_ref: None,
133 opts: opts.to_owned(),
134 header_row_index: None,
138 body_start_index: 0,
139 }
140 }
141
142
143 pub fn multimode(&self) -> bool {
144 matches!(self.data, SpreadData::Multiple(_))
145 }
146
147 pub fn to_json(&self) -> Value {
149 let mut result = json!({
150 "name": self.filename,
151 "extension": self.extension,
152 "selected": self.selected.clone().unwrap_or(vec![]),
153 "sheets": self.sheets,
154 "num_rows": self.num_rows,
155 "fields": self.keys,
156 "multimode": self.multimode(),
157 "data": self.data.to_json(),
158 "opts": self.opts.to_json()
159 });
160 if let Some(out_ref_str) = self.out_ref.clone() {
161 result["outref"] = json!(out_ref_str);
162 }
163 result
164 }
165
166 pub fn to_output_lines(&self, json_lines: bool) -> Vec<String> {
168 let selected_names = self.selected.clone().unwrap_or(vec![]);
169 let num_selected = selected_names.len();
170 let plural = if num_selected > 1 {
171 "s"
172 } else {
173 ""
174 };
175 let mut lines = vec![
176 format!("name:{}", self.filename),
177 format!("extension: {}", self.extension),
178
179 format!("sheets: {}", self.sheets.join(", ")),
180 ];
181 if num_selected > 0 {
182 lines.push(format!("selected sheet{}: {}", plural, selected_names.join(", ")));
183 }
184 lines.push(format!("row count: {}", self.num_rows));
185 lines.push(format!("fields: {}", self.keys.join(",")));
186 lines.push(format!("multimode: {}", self.multimode()));
187 for opt_line in self.opts.to_lines() {
188 lines.push(opt_line);
189 }
190 if let Some(out_ref_str) = self.out_ref.clone() {
191 lines.push(format!("output reference: {}", out_ref_str));
192 } else {
193 let has_many_sheets = self.sheets.len() > 1;
194 if !has_many_sheets || !self.multimode() {
195 lines.push("data:".to_owned());
196 }
197 if json_lines {
198 for sheet in &self.data.sheets() {
199 if has_many_sheets {
200 lines.push(format!("Sheet `{}` ({}):", sheet.name(), sheet.num_rows));
201 }
202 for item in &sheet.rows {
203 lines.push(format!("{}", json!(item)));
204 }
205 }
206 } else {
207 if self.multimode() {
208 for sheet in self.data.sheets() {
209 lines.push(format!("Sheet `{}` ({}):", sheet.name(), sheet.num_rows));
210 lines.push(format!("{}", json!(sheet)));
211 }
212 } else {
213 lines.push(format!("{}", self.data.to_json()));
214 }
215 }
216 }
217 lines
218 }
219
220 pub fn to_vec(&self) -> Vec<IndexMap<String, Value>> {
223 self.data.first_sheet().clone()
224 }
225
226 pub fn json_data(&self) -> Value {
228 json!(self.data)
229 }
230
231 pub fn rows(&self) -> Vec<String> {
233 let sheet = self.data.first_sheet();
234 let mut lines = Vec::with_capacity(sheet.len());
235 for row in &sheet {
236 lines.push(json!(row).to_string());
237 }
238 lines
239 }
240
241}
242
243#[derive(Debug, Clone, Serialize)]
244pub struct SheetDataSet {
245 pub sheet: (String, String),
246 pub num_rows: usize,
247 pub keys: Vec<String>,
248 pub rows: Vec<IndexMap<String, Value>>
249}
250
251impl SheetDataSet {
252
253
254
255 pub fn new(name: &str, keys: &[String], rows: &[IndexMap<String, Value>], total: usize) -> Self {
256 Self {
257 sheet: (name.to_string(), name.to_snake_case()),
258 keys: keys.to_vec(),
259 rows: rows.to_vec(),
260 num_rows: total
261 }
262 }
263
264 pub fn key(&self) -> String {
265 self.sheet.1.clone()
266 }
267
268 pub fn name(&self) -> String {
269 self.sheet.0.clone()
270 }
271}
272
273#[derive(Debug, Clone, Serialize)]
274pub enum SpreadData {
275 Single(Vec<IndexMap<String, Value>>),
276 Multiple(Vec<SheetDataSet>)
277}
278
279impl SpreadData {
280 pub fn from_single(rows: Vec<IndexMap<String, Value>>) -> Self {
281 SpreadData::Single(rows)
282 }
283
284 pub fn from_multiple(sheet_data: &[SheetDataSet]) -> Self {
285 SpreadData::Multiple(sheet_data.to_owned())
286 }
287
288 pub fn first_sheet(&self) -> Vec<IndexMap<String, Value>> {
289 match self {
290 SpreadData::Single(rows) => rows.to_owned(),
291 SpreadData::Multiple(sheets) => {
292 if let Some(sheet) = sheets.first() {
293 sheet.rows.to_owned()
294 } else {
295 vec![]
296 }
297 }
298 }
299 }
300
301 pub fn sheets(&self) -> Vec<SheetDataSet> {
303 match self {
304 SpreadData::Single(_) => vec![],
305 SpreadData::Multiple(sheets) => sheets.to_owned()
306 }
307 }
308
309 pub fn to_json(&self) -> Value {
310 match self {
311 SpreadData::Single(sheet) => json!(sheet),
312 SpreadData::Multiple(sheet_map) => json!(sheet_map)
313 }
314 }
315}
316
317
318#[derive(Debug, Clone, Serialize)]
319pub enum DataSet {
320 WithRows(usize, Vec<IndexMap<String, Value>>),
321 Count(usize)
322}
323
324impl DataSet {
325 pub fn from_count_and_rows(count: usize, rows: Vec<IndexMap<String, Value>>, opts: &OptionSet) -> Self {
326 match opts.read_mode() {
327 ReadMode::Sync | ReadMode::PreviewMultiple => DataSet::WithRows(count, rows),
328 ReadMode::Async => DataSet::Count(count),
329 }
330 }
331}
332
333
334pub fn to_index_map(row: &[serde_json::Value], headers: &[String], columns: Option<&[Column]>) -> IndexMap<String, Value> {
341 let mut hm: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
342 for (sub_index, hk) in headers.iter().enumerate() {
343 if let Some(cell) = row.get(sub_index) {
344 let segment = columns.and_then(|cols| cols.get(sub_index)).and_then(|c| c.key.as_ref());
345 match segment {
346 Some(key_segment) => insert_key_segment(&mut hm, key_segment, cell.to_owned()),
347 None => {
348 hm.insert(hk.to_owned(), cell.to_owned());
349 }
350 }
351 }
352 }
353 hm.into_iter().collect()
354}
355
356pub fn omit_null_values(row: &mut IndexMap<String, Value>) {
364 row.retain(|_, v| !v.is_null());
365 for v in row.values_mut() {
366 strip_nested_nulls(v);
367 }
368}
369
370fn strip_nested_nulls(value: &mut Value) {
371 match value {
372 Value::Object(map) => {
373 map.retain(|_, v| !v.is_null());
374 for v in map.values_mut() {
375 strip_nested_nulls(v);
376 }
377 }
378 Value::Array(items) => {
379 for item in items.iter_mut() {
380 strip_nested_nulls(item);
381 }
382 }
383 _ => {}
384 }
385}
386
387pub fn match_sheet_name_and_index(workbook: &mut Sheets<BufReader<File>>, opts: &OptionSet) -> (Vec<String>, Vec<String>, Vec<usize>) {
388 let mut sheet_indices = vec![];
389 let mut selected_names: Vec<String> = vec![];
390 let sheet_names = workbook.worksheets().into_iter().map(|ws| ws.0).collect::<Vec<String>>();
391 if let Some(sheet_keys) = opts.selected.clone() {
392 for sheet_key in sheet_keys {
393 if let Some(sheet_index) = sheet_names.iter().position(|s| s.to_snake_case() == sheet_key.to_snake_case()) {
394 sheet_indices.push(sheet_index);
395 selected_names.push(sheet_names[sheet_index].clone());
396 }
397 }
398 }
399 if sheet_indices.is_empty() && !opts.indices.is_empty() {
400 for s_index in opts.indices.clone() {
401 let sheet_index = s_index as usize;
402 if let Some(sheet_name) = sheet_names.get(sheet_index) {
403 sheet_indices.push(sheet_index);
404 selected_names.push(sheet_name.to_owned());
405 }
406 }
407 }
408 if sheet_indices.is_empty() {
409 sheet_indices = vec![0];
410 if !sheet_names.is_empty() {
411 selected_names.push(sheet_names[0].clone());
412 }
413 }
414 (selected_names, sheet_names, sheet_indices)
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use calamine::open_workbook_auto;
421
422 const SAMPLE_PATH: &str = "data/sample-data-2.ods";
424
425 fn opts_selecting(sheet_key: &str) -> OptionSet {
426 OptionSet::new(SAMPLE_PATH).sheet_name(sheet_key)
427 }
428
429 #[test]
430 fn test_omit_null_values_drops_top_level_and_nested_nulls_but_not_empty_strings() {
431 let mut row: IndexMap<String, Value> = serde_json::from_value(serde_json::json!({
432 "title": "Title 1",
433 "notes": "",
434 "download_2": null,
435 "measurements": {"weight": 60, "height": null},
436 "tags": ["a", null, "b"]
437 })).unwrap();
438 omit_null_values(&mut row);
439 assert_eq!(
440 serde_json::to_value(&row).unwrap(),
441 serde_json::json!({
442 "title": "Title 1",
443 "notes": "",
444 "measurements": {"weight": 60},
445 "tags": ["a", null, "b"]
448 })
449 );
450 }
451
452 #[test]
453 fn test_sheet_name_matching_is_case_insensitive_and_ignores_punctuation() {
454 let mut workbook = open_workbook_auto(SAMPLE_PATH).unwrap();
455 for variant in ["results 1", "RESULTS 1", "Results_1", "results-1", " results 1 "] {
458 let opts = opts_selecting(variant);
459 let (selected_names, _sheet_names, sheet_indices) = match_sheet_name_and_index(&mut workbook, &opts);
460 assert_eq!(selected_names, vec!["results 1".to_string()], "variant '{}' should match 'results 1'", variant);
461 assert_eq!(sheet_indices, vec![1], "variant '{}' should resolve to index 1", variant);
462 }
463 }
464
465 #[test]
466 fn test_sheet_name_matching_handles_names_with_no_spaces() {
467 let mut workbook = open_workbook_auto(SAMPLE_PATH).unwrap();
470 for variant in ["Rsults-2", "rsults-2", "RSULTS-2", "rsults_2"] {
471 let opts = opts_selecting(variant);
472 let (selected_names, _sheet_names, sheet_indices) = match_sheet_name_and_index(&mut workbook, &opts);
473 assert_eq!(selected_names, vec!["Rsults-2".to_string()], "variant '{}' should match 'Rsults-2'", variant);
474 assert_eq!(sheet_indices, vec![0]);
475 }
476 }
477
478 #[test]
479 fn test_sheet_name_matching_falls_back_to_first_sheet_when_unmatched() {
480 let mut workbook = open_workbook_auto(SAMPLE_PATH).unwrap();
481 let opts = opts_selecting("nonexistent sheet name");
482 let (selected_names, sheet_names, sheet_indices) = match_sheet_name_and_index(&mut workbook, &opts);
483 assert_eq!(sheet_names, vec!["Rsults-2".to_string(), "results 1".to_string()]);
484 assert_eq!(selected_names, vec!["Rsults-2".to_string()]);
486 assert_eq!(sheet_indices, vec![0]);
487 }
488
489 #[test]
490 fn test_sheet_index_selection_still_works() {
491 let mut workbook = open_workbook_auto(SAMPLE_PATH).unwrap();
492 let opts = OptionSet::new(SAMPLE_PATH).sheet_index(1);
493 let (selected_names, _sheet_names, sheet_indices) = match_sheet_name_and_index(&mut workbook, &opts);
494 assert_eq!(selected_names, vec!["results 1".to_string()]);
495 assert_eq!(sheet_indices, vec![1]);
496 }
497}
498