Skip to main content

sheeter_engine/
canonical.rs

1use std::error::Error;
2use std::io::Cursor;
3
4/// Format-agnostic intermediate value used by the Sheeter hub.
5///
6/// Every reader (CSV, TSV, JSON, YAML, XML) produces a `CanonicalValue`, and
7/// every writer consumes one. Adding a format requires exactly one reader and
8/// one writer, not `N-1` pairwise converters, because every spoke composes
9/// through this hub.
10///
11/// Tabular data is represented as `Array<Object>`. XML documents are
12/// represented as a single-key `Object` whose key is the root element name,
13/// with attributes prefixed by `@` and text stored under `#text`. Repeated
14/// sibling elements become `Array`.
15///
16/// # Examples
17///
18/// ```
19/// use sheeter_engine::CanonicalValue;
20///
21/// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
22/// assert_eq!(v.to_csv().unwrap(), "a\n1\n");
23/// ```
24#[derive(Debug, Clone, PartialEq)]
25pub enum CanonicalValue {
26    Null,
27    Bool(bool),
28    Number(serde_json::Number),
29    String(String),
30    Array(Vec<CanonicalValue>),
31    Object(Vec<(String, CanonicalValue)>),
32}
33
34type XmlStackFrame = (
35    String,
36    Vec<(String, String)>,
37    Vec<(String, CanonicalValue)>,
38    String,
39);
40
41impl CanonicalValue {
42    fn from_json_value(v: serde_json::Value) -> Self {
43        match v {
44            serde_json::Value::Null => Self::Null,
45            serde_json::Value::Bool(b) => Self::Bool(b),
46            serde_json::Value::Number(n) => Self::Number(n),
47            serde_json::Value::String(s) => Self::String(s),
48            serde_json::Value::Array(a) => {
49                Self::Array(a.into_iter().map(Self::from_json_value).collect())
50            }
51            serde_json::Value::Object(m) => Self::Object(
52                m.into_iter()
53                    .map(|(k, v)| (k, Self::from_json_value(v)))
54                    .collect(),
55            ),
56        }
57    }
58
59    fn to_json_value(&self) -> serde_json::Value {
60        match self {
61            Self::Null => serde_json::Value::Null,
62            Self::Bool(b) => serde_json::Value::Bool(*b),
63            Self::Number(n) => serde_json::Value::Number(n.clone()),
64            Self::String(s) => serde_json::Value::String(s.clone()),
65            Self::Array(a) => {
66                serde_json::Value::Array(a.iter().map(|v| v.to_json_value()).collect())
67            }
68            Self::Object(o) => {
69                let mut map = serde_json::Map::new();
70                for (k, v) in o {
71                    map.insert(k.clone(), v.to_json_value());
72                }
73                serde_json::Value::Object(map)
74            }
75        }
76    }
77
78    /// Parses JSON into the canonical hub.
79    ///
80    /// # Errors
81    /// Returns an error if the JSON is malformed.
82    ///
83    /// # Examples
84    /// ```
85    /// use sheeter_engine::CanonicalValue;
86    /// assert!(CanonicalValue::from_json(r#"{"a":1}"#).is_ok());
87    /// ```
88    pub fn from_json(input: &str) -> Result<Self, Box<dyn Error>> {
89        Ok(Self::from_json_value(serde_json::from_str(input)?))
90    }
91
92    /// Serializes the canonical hub to JSON.
93    ///
94    /// # Examples
95    /// ```
96    /// use sheeter_engine::CanonicalValue;
97    /// let v = CanonicalValue::String("x".into());
98    /// assert_eq!(v.to_json().unwrap(), r#""x""#);
99    /// ```
100    pub fn to_json(&self) -> Result<String, Box<dyn Error>> {
101        Ok(serde_json::to_string(&self.to_json_value())?)
102    }
103
104    fn from_delimited(input: &str, delim: u8) -> Result<Self, Box<dyn Error>> {
105        let input = input.strip_prefix('\u{feff}').unwrap_or(input);
106        let mut rdr = csv::ReaderBuilder::new()
107            .delimiter(delim)
108            .from_reader(Cursor::new(input));
109        let headers = rdr.headers()?.clone();
110        let mut seen = std::collections::HashSet::new();
111        for h in headers.iter() {
112            if !seen.insert(h) {
113                return Err(format!("duplicate header: {h}").into());
114            }
115        }
116        let mut rows = Vec::new();
117        for result in rdr.records() {
118            let record = result?;
119            let mut obj = Vec::with_capacity(headers.len());
120            for (h, v) in headers.iter().zip(record.iter()) {
121                obj.push((h.to_string(), Self::String(v.to_string())));
122            }
123            rows.push(Self::Object(obj));
124        }
125        Ok(Self::Array(rows))
126    }
127
128    fn to_delimited(&self, delim: u8) -> Result<String, Box<dyn Error>> {
129        let rows = self.as_tabular()?;
130        if rows.is_empty() {
131            return Ok(String::new());
132        }
133        let mut headers: Vec<String> = Vec::new();
134        for row in &rows {
135            let obj = row.as_object()?;
136            for (k, _) in obj {
137                if !headers.contains(k) {
138                    headers.push(k.clone());
139                }
140            }
141        }
142        let mut wtr = csv::WriterBuilder::new()
143            .delimiter(delim)
144            .from_writer(vec![]);
145        wtr.write_record(&headers)?;
146        for row in &rows {
147            let obj = row.as_object()?;
148            let mut record = Vec::with_capacity(headers.len());
149            for h in &headers {
150                let cell = match obj.iter().find(|(k, _)| k == h) {
151                    Some((_, Self::Null)) | None => String::new(),
152                    Some((_, Self::Bool(b))) => b.to_string(),
153                    Some((_, Self::Number(n))) => n.to_string(),
154                    Some((_, Self::String(s))) => s.clone(),
155                    Some((_, Self::Array(_))) | Some((_, Self::Object(_))) => {
156                        return Err(format!(
157                            "nested value at key {h} cannot be represented in tabular output"
158                        )
159                        .into());
160                    }
161                };
162                record.push(cell);
163            }
164            wtr.write_record(&record)?;
165        }
166        let data = wtr.into_inner()?;
167        Ok(String::from_utf8(data)?)
168    }
169
170    pub fn from_toml(input: &str) -> Result<Self, Box<dyn Error>> {
171        let value: toml::Value = toml::from_str(input)?;
172        Ok(Self::from_toml_value(value))
173    }
174
175    fn from_toml_value(v: toml::Value) -> Self {
176        match v {
177            toml::Value::String(s) => Self::String(s),
178            toml::Value::Integer(i) => Self::Number(serde_json::Number::from(i)),
179            toml::Value::Float(f) => match serde_json::Number::from_f64(f) {
180                Some(n) => Self::Number(n),
181                None => Self::String(f.to_string()),
182            },
183            toml::Value::Boolean(b) => Self::Bool(b),
184            toml::Value::Datetime(dt) => Self::String(dt.to_string()),
185            toml::Value::Array(a) => {
186                Self::Array(a.into_iter().map(Self::from_toml_value).collect())
187            }
188            toml::Value::Table(t) => Self::Object(
189                t.into_iter()
190                    .map(|(k, v)| (k, Self::from_toml_value(v)))
191                    .collect(),
192            ),
193        }
194    }
195
196    pub fn to_toml(&self) -> Result<String, Box<dyn Error>> {
197        let value = match self {
198            Self::Array(_) => {
199                let mut table = toml::map::Map::new();
200                table.insert("item".to_string(), self.to_toml_value()?);
201                toml::Value::Table(table)
202            }
203            Self::Object(_) => self.to_toml_value()?,
204            _ => {
205                let mut table = toml::map::Map::new();
206                table.insert("value".to_string(), self.to_toml_value()?);
207                toml::Value::Table(table)
208            }
209        };
210        Ok(toml::to_string_pretty(&value)?)
211    }
212
213    fn to_toml_value(&self) -> Result<toml::Value, Box<dyn Error>> {
214        Ok(match self {
215            Self::Null => return Err("TOML has no null type".into()),
216            Self::Bool(b) => toml::Value::Boolean(*b),
217            Self::Number(n) => {
218                if let Some(i) = n.as_i64() {
219                    toml::Value::Integer(i)
220                } else if let Some(u) = n.as_u64() {
221                    if u <= i64::MAX as u64 {
222                        toml::Value::Integer(u as i64)
223                    } else {
224                        return Err(format!("TOML integer out of range: {u}").into());
225                    }
226                } else if let Some(f) = n.as_f64() {
227                    toml::Value::Float(f)
228                } else {
229                    return Err(format!("cannot represent number {n} in TOML").into());
230                }
231            }
232            Self::String(s) => toml::Value::String(s.clone()),
233            Self::Array(a) => {
234                let mut items = Vec::with_capacity(a.len());
235                for item in a {
236                    if let Self::Null = item {
237                        return Err("TOML arrays cannot contain null".into());
238                    }
239                    items.push(item.to_toml_value()?);
240                }
241                toml::Value::Array(items)
242            }
243            Self::Object(o) => {
244                let mut table = toml::map::Map::new();
245                for (k, v) in o {
246                    if let Self::Null = v {
247                        continue;
248                    }
249                    table.insert(k.clone(), v.to_toml_value()?);
250                }
251                toml::Value::Table(table)
252            }
253        })
254    }
255
256    pub fn from_xlsx_workbook(bytes: &[u8]) -> Result<Vec<(String, Self)>, Box<dyn Error>> {
257        use calamine::{Reader, Xlsx};
258        let cursor = Cursor::new(bytes.to_vec());
259        let mut workbook: Xlsx<_> = Xlsx::new(cursor)?;
260        let names = workbook.sheet_names().to_owned();
261        let mut out = Vec::with_capacity(names.len());
262        for name in names {
263            let range = workbook
264                .worksheet_range(&name)
265                .map_err(|e| format!("failed to read sheet {name}: {e:?}"))?;
266            out.push((name, Self::from_calamine_range(&range)?));
267        }
268        Ok(out)
269    }
270
271    fn from_calamine_range(
272        range: &calamine::Range<calamine::Data>,
273    ) -> Result<Self, Box<dyn Error>> {
274        use calamine::Data;
275        let mut rows = range.rows();
276        let header = match rows.next() {
277            Some(h) => h.iter().map(cell_to_string).collect::<Vec<String>>(),
278            None => return Ok(Self::Array(Vec::new())),
279        };
280        let mut records = Vec::new();
281        for row in rows {
282            if row.iter().all(|c| matches!(c, Data::Empty)) {
283                continue;
284            }
285            let mut obj = Vec::with_capacity(header.len());
286            for (i, h) in header.iter().enumerate() {
287                let val = match row.get(i) {
288                    Some(Data::Empty) | None => Self::Null,
289                    Some(Data::String(s)) => Self::String(s.clone()),
290                    Some(Data::Float(f)) => match serde_json::Number::from_f64(*f) {
291                        Some(n) => Self::Number(n),
292                        None => Self::String(f.to_string()),
293                    },
294                    Some(Data::Int(i)) => Self::Number(serde_json::Number::from(*i)),
295                    Some(Data::Bool(b)) => Self::Bool(*b),
296                    Some(Data::DateTime(dt)) => Self::String(dt.to_string()),
297                    Some(Data::DateTimeIso(s)) => Self::String(s.clone()),
298                    Some(Data::DurationIso(s)) => Self::String(s.clone()),
299                    Some(Data::Error(e)) => Self::String(format!("{e:?}")),
300                };
301                obj.push((h.clone(), val));
302            }
303            records.push(Self::Object(obj));
304        }
305        Ok(Self::Array(records))
306    }
307
308    pub fn to_xlsx_workbook(sheets: &[(String, Self)]) -> Result<Vec<u8>, Box<dyn Error>> {
309        use rust_xlsxwriter::Workbook;
310        let mut wb = Workbook::new();
311        for (name, value) in sheets {
312            let ws = wb.add_worksheet();
313            ws.set_name(name.as_str())?;
314            let rows = value.as_tabular()?;
315            if rows.is_empty() {
316                continue;
317            }
318            let mut headers: Vec<String> = Vec::new();
319            for row in &rows {
320                let obj = row.as_object()?;
321                for (k, _) in obj {
322                    if !headers.contains(k) {
323                        headers.push(k.clone());
324                    }
325                }
326            }
327            for (col, h) in headers.iter().enumerate() {
328                ws.write_string(0, col as u16, h)?;
329            }
330            for (i, row) in rows.iter().enumerate() {
331                let obj = row.as_object()?;
332                for (col, h) in headers.iter().enumerate() {
333                    let row_idx = (i + 1) as u32;
334                    let col_idx = col as u16;
335                    match obj.iter().find(|(k, _)| k == h) {
336                        Some((_, Self::String(s))) => {
337                            ws.write_string(row_idx, col_idx, s)?;
338                        }
339                        Some((_, Self::Number(n))) => {
340                            if let Some(f) = n.as_f64() {
341                                ws.write_number(row_idx, col_idx, f)?;
342                            } else {
343                                ws.write_string(row_idx, col_idx, n.to_string())?;
344                            }
345                        }
346                        Some((_, Self::Bool(b))) => {
347                            ws.write_boolean(row_idx, col_idx, *b)?;
348                        }
349                        Some((_, Self::Null)) | None => {}
350                        Some((_, Self::Array(_))) | Some((_, Self::Object(_))) => {
351                            return Err(format!(
352                                "nested value at key {h} cannot be represented in XLSX"
353                            )
354                            .into());
355                        }
356                    }
357                }
358            }
359        }
360        Ok(wb.save_to_buffer()?)
361    }
362
363    fn as_object(&self) -> Result<&Vec<(String, CanonicalValue)>, Box<dyn Error>> {
364        match self {
365            Self::Object(o) => Ok(o),
366            _ => Err("expected object".into()),
367        }
368    }
369
370    fn as_tabular(&self) -> Result<Vec<&CanonicalValue>, Box<dyn Error>> {
371        if let Some(arr) = find_first_object_array(self) {
372            return Ok(arr.iter().collect());
373        }
374        let mut current = self;
375        while let Self::Object(o) = current {
376            if o.len() != 1 {
377                break;
378            }
379            match &o[0].1 {
380                Self::Object(_) => current = &o[0].1,
381                _ => break,
382            }
383        }
384        if let Self::Object(o) = current {
385            if o.iter().all(|(_, v)| is_scalar(v)) {
386                return Ok(vec![current]);
387            }
388        }
389        Err("cannot determine tabular structure".into())
390    }
391
392    /// Parses CSV into the canonical hub.
393    ///
394    /// # Examples
395    /// ```
396    /// use sheeter_engine::CanonicalValue;
397    /// let v = CanonicalValue::from_csv("a\n1").unwrap();
398    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":"1"}]"#);
399    /// ```
400    pub fn from_csv(input: &str) -> Result<Self, Box<dyn Error>> {
401        Self::from_delimited(input, b',')
402    }
403
404    /// Serializes the canonical hub to CSV.
405    ///
406    /// # Examples
407    /// ```
408    /// use sheeter_engine::CanonicalValue;
409    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
410    /// assert_eq!(v.to_csv().unwrap(), "a\n1\n");
411    /// ```
412    pub fn to_csv(&self) -> Result<String, Box<dyn Error>> {
413        self.to_delimited(b',')
414    }
415
416    /// Parses TSV into the canonical hub.
417    ///
418    /// # Examples
419    /// ```
420    /// use sheeter_engine::CanonicalValue;
421    /// let v = CanonicalValue::from_tsv("a\tb\n1\t2").unwrap();
422    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":"1","b":"2"}]"#);
423    /// ```
424    pub fn from_tsv(input: &str) -> Result<Self, Box<dyn Error>> {
425        Self::from_delimited(input, b'\t')
426    }
427
428    /// Serializes the canonical hub to TSV.
429    ///
430    /// # Examples
431    /// ```
432    /// use sheeter_engine::CanonicalValue;
433    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
434    /// assert_eq!(v.to_tsv().unwrap(), "a\n1\n");
435    /// ```
436    pub fn to_tsv(&self) -> Result<String, Box<dyn Error>> {
437        self.to_delimited(b'\t')
438    }
439
440    /// Parses YAML into the canonical hub.
441    ///
442    /// # Examples
443    /// ```
444    /// use sheeter_engine::CanonicalValue;
445    /// let v = CanonicalValue::from_yaml("- a: 1\n").unwrap();
446    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":1}]"#);
447    /// ```
448    pub fn from_yaml(input: &str) -> Result<Self, Box<dyn Error>> {
449        let opts = serde_saphyr::options! {
450            strict_booleans: true,
451            ignore_binary_tag_for_string: true,
452        };
453        let v: serde_json::Value = serde_saphyr::from_str_with_options(input, opts)?;
454        Ok(Self::from_json_value(v))
455    }
456
457    /// Serializes the canonical hub to YAML.
458    ///
459    /// # Examples
460    /// ```
461    /// use sheeter_engine::CanonicalValue;
462    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
463    /// assert!(v.to_yaml().unwrap().contains("a: 1"));
464    /// ```
465    pub fn to_yaml(&self) -> Result<String, Box<dyn Error>> {
466        Ok(serde_saphyr::to_string(&self.to_json_value())?)
467    }
468
469    /// Parses XML into the canonical hub.
470    ///
471    /// Attributes become `@name` keys, text content becomes `#text`, and
472    /// repeated sibling elements become arrays. The returned value is an
473    /// object with exactly one key: the root element name.
474    ///
475    /// # Examples
476    /// ```
477    /// use sheeter_engine::CanonicalValue;
478    /// let v = CanonicalValue::from_xml("<r><a>1</a><a>2</a></r>").unwrap();
479    /// assert_eq!(v.to_json().unwrap(), r#"{"r":{"a":["1","2"]}}"#);
480    /// ```
481    pub fn from_xml(input: &str) -> Result<Self, Box<dyn Error>> {
482        use quick_xml::events::Event;
483        use quick_xml::Reader;
484
485        let mut reader = Reader::from_str(input);
486        reader.config_mut().trim_text(true);
487        let mut stack: Vec<XmlStackFrame> = Vec::new();
488        let mut root: Option<(String, CanonicalValue)> = None;
489
490        loop {
491            match reader.read_event() {
492                Ok(Event::Start(e)) => {
493                    let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
494                    let mut attrs = Vec::new();
495                    for a in e.attributes() {
496                        let a = a?;
497                        attrs.push((
498                            String::from_utf8_lossy(a.key.as_ref()).to_string(),
499                            a.unescape_value()?.to_string(),
500                        ));
501                    }
502                    stack.push((name, attrs, Vec::new(), String::new()));
503                }
504                Ok(Event::Text(e)) => {
505                    if let Some(top) = stack.last_mut() {
506                        top.3.push_str(&e.unescape()?);
507                    }
508                }
509                Ok(Event::End(_)) => {
510                    let (name, attributes, children, text) = stack.pop().ok_or("unbalanced XML")?;
511                    let trimmed = text.trim().to_string();
512                    let value = if attributes.is_empty() && children.is_empty() {
513                        Self::String(trimmed)
514                    } else {
515                        let mut obj: Vec<(String, CanonicalValue)> = Vec::new();
516                        for (k, v) in attributes {
517                            obj.push((format!("@{k}"), Self::String(v)));
518                        }
519                        if !trimmed.is_empty() {
520                            obj.push(("#text".to_string(), Self::String(trimmed)));
521                        }
522                        for (cname, cval) in children {
523                            if let Some(slot) = obj.iter_mut().find(|(k, _)| k == &cname) {
524                                match &mut slot.1 {
525                                    Self::Array(a) => a.push(cval),
526                                    _ => {
527                                        let old = slot.1.clone();
528                                        slot.1 = Self::Array(vec![old, cval]);
529                                    }
530                                }
531                            } else {
532                                obj.push((cname, cval));
533                            }
534                        }
535                        Self::Object(obj)
536                    };
537                    if let Some(parent) = stack.last_mut() {
538                        parent.2.push((name, value));
539                    } else {
540                        root = Some((name, value));
541                    }
542                }
543                Ok(Event::Eof) => break,
544                Ok(_) => {}
545                Err(e) => return Err(e.into()),
546            }
547        }
548
549        let (root_name, root_value) = root.ok_or("empty XML")?;
550        Ok(Self::Object(vec![(root_name, root_value)]))
551    }
552
553    /// Serializes the canonical hub to XML.
554    ///
555    /// # Examples
556    /// ```
557    /// use sheeter_engine::CanonicalValue;
558    /// let v = CanonicalValue::from_json(r#"[{"a":"1"}]"#).unwrap();
559    /// let xml = v.to_xml().unwrap();
560    /// assert!(xml.contains("<root>"));
561    /// assert!(xml.contains("<a>1</a>"));
562    /// ```
563    pub fn to_xml(&self) -> Result<String, Box<dyn Error>> {
564        let mut writer = quick_xml::Writer::new_with_indent(Vec::new(), b' ', 2);
565        match self {
566            Self::Object(o) if o.len() == 1 && !o[0].0.starts_with('@') && o[0].0 != "#text" => {
567                write_element(&mut writer, &o[0].0, &o[0].1)?;
568            }
569            Self::Array(a) => {
570                write_open(&mut writer, "root", &[])?;
571                for item in a {
572                    write_element(&mut writer, "item", item)?;
573                }
574                write_close(&mut writer, "root")?;
575            }
576            _ => write_element(&mut writer, "root", self)?,
577        }
578        Ok(String::from_utf8(writer.into_inner())?)
579    }
580}
581
582fn write_open<W: std::io::Write>(
583    writer: &mut quick_xml::Writer<W>,
584    name: &str,
585    attrs: &[(String, String)],
586) -> Result<(), Box<dyn Error>> {
587    use quick_xml::events::{BytesStart, Event};
588    let mut start = BytesStart::new(name);
589    for (k, v) in attrs {
590        start.push_attribute((k.as_str(), v.as_str()));
591    }
592    writer.write_event(Event::Start(start))?;
593    Ok(())
594}
595
596fn write_close<W: std::io::Write>(
597    writer: &mut quick_xml::Writer<W>,
598    name: &str,
599) -> Result<(), Box<dyn Error>> {
600    use quick_xml::events::{BytesEnd, Event};
601    writer.write_event(Event::End(BytesEnd::new(name)))?;
602    Ok(())
603}
604
605fn write_element<W: std::io::Write>(
606    writer: &mut quick_xml::Writer<W>,
607    name: &str,
608    value: &CanonicalValue,
609) -> Result<(), Box<dyn Error>> {
610    use quick_xml::events::{BytesText, Event};
611
612    match value {
613        CanonicalValue::Array(items) => {
614            for item in items {
615                write_element(writer, name, item)?;
616            }
617        }
618        CanonicalValue::Object(o) => {
619            let attrs: Vec<(String, String)> = o
620                .iter()
621                .filter_map(|(k, v)| {
622                    k.strip_prefix('@').and_then(|attr| match v {
623                        CanonicalValue::String(s) => Some((attr.to_string(), s.clone())),
624                        _ => None,
625                    })
626                })
627                .collect();
628            write_open(writer, name, &attrs)?;
629            for (k, v) in o {
630                if k.starts_with('@') {
631                    continue;
632                }
633                if k == "#text" {
634                    if let CanonicalValue::String(s) = v {
635                        writer.write_event(Event::Text(BytesText::new(s)))?;
636                    }
637                    continue;
638                }
639                write_element(writer, k, v)?;
640            }
641            write_close(writer, name)?;
642        }
643        CanonicalValue::String(s) => {
644            write_open(writer, name, &[])?;
645            writer.write_event(Event::Text(BytesText::new(s)))?;
646            write_close(writer, name)?;
647        }
648        CanonicalValue::Number(n) => {
649            write_open(writer, name, &[])?;
650            writer.write_event(Event::Text(BytesText::new(&n.to_string())))?;
651            write_close(writer, name)?;
652        }
653        CanonicalValue::Bool(b) => {
654            write_open(writer, name, &[])?;
655            writer.write_event(Event::Text(BytesText::new(&b.to_string())))?;
656            write_close(writer, name)?;
657        }
658        CanonicalValue::Null => {
659            write_open(writer, name, &[])?;
660            write_close(writer, name)?;
661        }
662    }
663    Ok(())
664}
665
666fn is_scalar(v: &CanonicalValue) -> bool {
667    matches!(
668        v,
669        CanonicalValue::Null
670            | CanonicalValue::Bool(_)
671            | CanonicalValue::Number(_)
672            | CanonicalValue::String(_)
673    )
674}
675
676fn find_first_object_array(v: &CanonicalValue) -> Option<&Vec<CanonicalValue>> {
677    match v {
678        CanonicalValue::Array(a)
679            if !a.is_empty() && a.iter().all(|x| matches!(x, CanonicalValue::Object(_))) =>
680        {
681            Some(a)
682        }
683        CanonicalValue::Object(o) => {
684            for (_, v) in o {
685                if let Some(arr) = find_first_object_array(v) {
686                    return Some(arr);
687                }
688            }
689            None
690        }
691        _ => None,
692    }
693}
694
695fn cell_to_string(c: &calamine::Data) -> String {
696    use calamine::Data;
697    match c {
698        Data::Empty => String::new(),
699        Data::String(s) => s.clone(),
700        Data::Float(f) => f.to_string(),
701        Data::Int(i) => i.to_string(),
702        Data::Bool(b) => b.to_string(),
703        Data::DateTime(dt) => dt.to_string(),
704        Data::DateTimeIso(s) => s.clone(),
705        Data::DurationIso(s) => s.clone(),
706        Data::Error(e) => format!("{e:?}"),
707    }
708}