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)) => {
291                        if f.is_finite() && f.fract() == 0.0 && f.abs() < i64::MAX as f64 {
292                            Self::Number(serde_json::Number::from(*f as i64))
293                        } else {
294                            match serde_json::Number::from_f64(*f) {
295                                Some(n) => Self::Number(n),
296                                None => Self::String(f.to_string()),
297                            }
298                        }
299                    }
300                    Some(Data::Int(i)) => Self::Number(serde_json::Number::from(*i)),
301                    Some(Data::Bool(b)) => Self::Bool(*b),
302                    Some(Data::DateTime(dt)) => Self::String(dt.to_string()),
303                    Some(Data::DateTimeIso(s)) => Self::String(s.clone()),
304                    Some(Data::DurationIso(s)) => Self::String(s.clone()),
305                    Some(Data::Error(e)) => Self::String(format!("{e:?}")),
306                };
307                obj.push((h.clone(), val));
308            }
309            records.push(Self::Object(obj));
310        }
311        Ok(Self::Array(records))
312    }
313
314    pub fn to_xlsx_workbook(sheets: &[(String, Self)]) -> Result<Vec<u8>, Box<dyn Error>> {
315        use rust_xlsxwriter::Workbook;
316        let mut wb = Workbook::new();
317        for (name, value) in sheets {
318            let ws = wb.add_worksheet();
319            ws.set_name(name.as_str())?;
320            let rows = value.as_tabular()?;
321            if rows.is_empty() {
322                continue;
323            }
324            let mut headers: Vec<String> = Vec::new();
325            for row in &rows {
326                let obj = row.as_object()?;
327                for (k, _) in obj {
328                    if !headers.contains(k) {
329                        headers.push(k.clone());
330                    }
331                }
332            }
333            for (col, h) in headers.iter().enumerate() {
334                ws.write_string(0, col as u16, h)?;
335            }
336            for (i, row) in rows.iter().enumerate() {
337                let obj = row.as_object()?;
338                for (col, h) in headers.iter().enumerate() {
339                    let row_idx = (i + 1) as u32;
340                    let col_idx = col as u16;
341                    match obj.iter().find(|(k, _)| k == h) {
342                        Some((_, Self::String(s))) => {
343                            ws.write_string(row_idx, col_idx, s)?;
344                        }
345                        Some((_, Self::Number(n))) => {
346                            if let Some(f) = n.as_f64() {
347                                ws.write_number(row_idx, col_idx, f)?;
348                            } else {
349                                ws.write_string(row_idx, col_idx, n.to_string())?;
350                            }
351                        }
352                        Some((_, Self::Bool(b))) => {
353                            ws.write_boolean(row_idx, col_idx, *b)?;
354                        }
355                        Some((_, Self::Null)) | None => {}
356                        Some((_, Self::Array(_))) | Some((_, Self::Object(_))) => {
357                            return Err(format!(
358                                "nested value at key {h} cannot be represented in XLSX"
359                            )
360                            .into());
361                        }
362                    }
363                }
364            }
365        }
366        Ok(wb.save_to_buffer()?)
367    }
368
369    fn as_object(&self) -> Result<&Vec<(String, CanonicalValue)>, Box<dyn Error>> {
370        match self {
371            Self::Object(o) => Ok(o),
372            _ => Err("expected object".into()),
373        }
374    }
375
376    fn as_tabular(&self) -> Result<Vec<&CanonicalValue>, Box<dyn Error>> {
377        if let Some(arr) = find_first_object_array(self) {
378            return Ok(arr.iter().collect());
379        }
380        let mut current = self;
381        while let Self::Object(o) = current {
382            if o.len() != 1 {
383                break;
384            }
385            match &o[0].1 {
386                Self::Object(_) => current = &o[0].1,
387                _ => break,
388            }
389        }
390        if let Self::Object(o) = current {
391            if o.iter().all(|(_, v)| is_scalar(v)) {
392                return Ok(vec![current]);
393            }
394        }
395        Err("cannot determine tabular structure".into())
396    }
397
398    /// Parses CSV into the canonical hub.
399    ///
400    /// # Examples
401    /// ```
402    /// use sheeter_engine::CanonicalValue;
403    /// let v = CanonicalValue::from_csv("a\n1").unwrap();
404    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":"1"}]"#);
405    /// ```
406    pub fn from_csv(input: &str) -> Result<Self, Box<dyn Error>> {
407        Self::from_delimited(input, b',')
408    }
409
410    /// Serializes the canonical hub to CSV.
411    ///
412    /// # Examples
413    /// ```
414    /// use sheeter_engine::CanonicalValue;
415    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
416    /// assert_eq!(v.to_csv().unwrap(), "a\n1\n");
417    /// ```
418    pub fn to_csv(&self) -> Result<String, Box<dyn Error>> {
419        self.to_delimited(b',')
420    }
421
422    /// Parses TSV into the canonical hub.
423    ///
424    /// # Examples
425    /// ```
426    /// use sheeter_engine::CanonicalValue;
427    /// let v = CanonicalValue::from_tsv("a\tb\n1\t2").unwrap();
428    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":"1","b":"2"}]"#);
429    /// ```
430    pub fn from_tsv(input: &str) -> Result<Self, Box<dyn Error>> {
431        Self::from_delimited(input, b'\t')
432    }
433
434    /// Serializes the canonical hub to TSV.
435    ///
436    /// # Examples
437    /// ```
438    /// use sheeter_engine::CanonicalValue;
439    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
440    /// assert_eq!(v.to_tsv().unwrap(), "a\n1\n");
441    /// ```
442    pub fn to_tsv(&self) -> Result<String, Box<dyn Error>> {
443        self.to_delimited(b'\t')
444    }
445
446    /// Parses YAML into the canonical hub.
447    ///
448    /// # Examples
449    /// ```
450    /// use sheeter_engine::CanonicalValue;
451    /// let v = CanonicalValue::from_yaml("- a: 1\n").unwrap();
452    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":1}]"#);
453    /// ```
454    pub fn from_yaml(input: &str) -> Result<Self, Box<dyn Error>> {
455        let opts = serde_saphyr::options! {
456            strict_booleans: true,
457            ignore_binary_tag_for_string: true,
458        };
459        let v: serde_json::Value = serde_saphyr::from_str_with_options(input, opts)?;
460        Ok(Self::from_json_value(v))
461    }
462
463    /// Serializes the canonical hub to YAML.
464    ///
465    /// # Examples
466    /// ```
467    /// use sheeter_engine::CanonicalValue;
468    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
469    /// assert!(v.to_yaml().unwrap().contains("a: 1"));
470    /// ```
471    pub fn to_yaml(&self) -> Result<String, Box<dyn Error>> {
472        Ok(serde_saphyr::to_string(&self.to_json_value())?)
473    }
474
475    /// Parses XML into the canonical hub.
476    ///
477    /// Attributes become `@name` keys, text content becomes `#text`, and
478    /// repeated sibling elements become arrays. The returned value is an
479    /// object with exactly one key: the root element name.
480    ///
481    /// # Examples
482    /// ```
483    /// use sheeter_engine::CanonicalValue;
484    /// let v = CanonicalValue::from_xml("<r><a>1</a><a>2</a></r>").unwrap();
485    /// assert_eq!(v.to_json().unwrap(), r#"{"r":{"a":["1","2"]}}"#);
486    /// ```
487    pub fn from_xml(input: &str) -> Result<Self, Box<dyn Error>> {
488        use quick_xml::events::Event;
489        use quick_xml::Reader;
490
491        let mut reader = Reader::from_str(input);
492        reader.config_mut().trim_text(true);
493        let mut stack: Vec<XmlStackFrame> = Vec::new();
494        let mut root: Option<(String, CanonicalValue)> = None;
495
496        loop {
497            match reader.read_event() {
498                Ok(Event::Start(e)) => {
499                    let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
500                    let mut attrs = Vec::new();
501                    for a in e.attributes() {
502                        let a = a?;
503                        attrs.push((
504                            String::from_utf8_lossy(a.key.as_ref()).to_string(),
505                            a.unescape_value()?.to_string(),
506                        ));
507                    }
508                    stack.push((name, attrs, Vec::new(), String::new()));
509                }
510                Ok(Event::Text(e)) => {
511                    if let Some(top) = stack.last_mut() {
512                        top.3.push_str(&e.unescape()?);
513                    }
514                }
515                Ok(Event::End(_)) => {
516                    let (name, attributes, children, text) = stack.pop().ok_or("unbalanced XML")?;
517                    let trimmed = text.trim().to_string();
518                    let value = if attributes.is_empty() && children.is_empty() {
519                        Self::String(trimmed)
520                    } else {
521                        let mut obj: Vec<(String, CanonicalValue)> = Vec::new();
522                        for (k, v) in attributes {
523                            obj.push((format!("@{k}"), Self::String(v)));
524                        }
525                        if !trimmed.is_empty() {
526                            obj.push(("#text".to_string(), Self::String(trimmed)));
527                        }
528                        for (cname, cval) in children {
529                            if let Some(slot) = obj.iter_mut().find(|(k, _)| k == &cname) {
530                                match &mut slot.1 {
531                                    Self::Array(a) => a.push(cval),
532                                    _ => {
533                                        let old = slot.1.clone();
534                                        slot.1 = Self::Array(vec![old, cval]);
535                                    }
536                                }
537                            } else {
538                                obj.push((cname, cval));
539                            }
540                        }
541                        Self::Object(obj)
542                    };
543                    if let Some(parent) = stack.last_mut() {
544                        parent.2.push((name, value));
545                    } else {
546                        root = Some((name, value));
547                    }
548                }
549                Ok(Event::Eof) => break,
550                Ok(_) => {}
551                Err(e) => return Err(e.into()),
552            }
553        }
554
555        let (root_name, root_value) = root.ok_or("empty XML")?;
556        Ok(Self::Object(vec![(root_name, root_value)]))
557    }
558
559    /// Serializes the canonical hub to XML.
560    ///
561    /// # Examples
562    /// ```
563    /// use sheeter_engine::CanonicalValue;
564    /// let v = CanonicalValue::from_json(r#"[{"a":"1"}]"#).unwrap();
565    /// let xml = v.to_xml().unwrap();
566    /// assert!(xml.contains("<root>"));
567    /// assert!(xml.contains("<a>1</a>"));
568    /// ```
569    pub fn to_xml(&self) -> Result<String, Box<dyn Error>> {
570        let mut writer = quick_xml::Writer::new_with_indent(Vec::new(), b' ', 2);
571        match self {
572            Self::Object(o) if o.len() == 1 && !o[0].0.starts_with('@') && o[0].0 != "#text" => {
573                write_element(&mut writer, &o[0].0, &o[0].1)?;
574            }
575            Self::Array(a) => {
576                write_open(&mut writer, "root", &[])?;
577                for item in a {
578                    write_element(&mut writer, "item", item)?;
579                }
580                write_close(&mut writer, "root")?;
581            }
582            _ => write_element(&mut writer, "root", self)?,
583        }
584        Ok(String::from_utf8(writer.into_inner())?)
585    }
586}
587
588fn write_open<W: std::io::Write>(
589    writer: &mut quick_xml::Writer<W>,
590    name: &str,
591    attrs: &[(String, String)],
592) -> Result<(), Box<dyn Error>> {
593    use quick_xml::events::{BytesStart, Event};
594    let mut start = BytesStart::new(name);
595    for (k, v) in attrs {
596        start.push_attribute((k.as_str(), v.as_str()));
597    }
598    writer.write_event(Event::Start(start))?;
599    Ok(())
600}
601
602fn write_close<W: std::io::Write>(
603    writer: &mut quick_xml::Writer<W>,
604    name: &str,
605) -> Result<(), Box<dyn Error>> {
606    use quick_xml::events::{BytesEnd, Event};
607    writer.write_event(Event::End(BytesEnd::new(name)))?;
608    Ok(())
609}
610
611fn write_element<W: std::io::Write>(
612    writer: &mut quick_xml::Writer<W>,
613    name: &str,
614    value: &CanonicalValue,
615) -> Result<(), Box<dyn Error>> {
616    use quick_xml::events::{BytesText, Event};
617
618    match value {
619        CanonicalValue::Array(items) => {
620            for item in items {
621                write_element(writer, name, item)?;
622            }
623        }
624        CanonicalValue::Object(o) => {
625            let attrs: Vec<(String, String)> = o
626                .iter()
627                .filter_map(|(k, v)| {
628                    k.strip_prefix('@').and_then(|attr| match v {
629                        CanonicalValue::String(s) => Some((attr.to_string(), s.clone())),
630                        _ => None,
631                    })
632                })
633                .collect();
634            write_open(writer, name, &attrs)?;
635            for (k, v) in o {
636                if k.starts_with('@') {
637                    continue;
638                }
639                if k == "#text" {
640                    if let CanonicalValue::String(s) = v {
641                        writer.write_event(Event::Text(BytesText::new(s)))?;
642                    }
643                    continue;
644                }
645                write_element(writer, k, v)?;
646            }
647            write_close(writer, name)?;
648        }
649        CanonicalValue::String(s) => {
650            write_open(writer, name, &[])?;
651            writer.write_event(Event::Text(BytesText::new(s)))?;
652            write_close(writer, name)?;
653        }
654        CanonicalValue::Number(n) => {
655            write_open(writer, name, &[])?;
656            writer.write_event(Event::Text(BytesText::new(&n.to_string())))?;
657            write_close(writer, name)?;
658        }
659        CanonicalValue::Bool(b) => {
660            write_open(writer, name, &[])?;
661            writer.write_event(Event::Text(BytesText::new(&b.to_string())))?;
662            write_close(writer, name)?;
663        }
664        CanonicalValue::Null => {
665            write_open(writer, name, &[])?;
666            write_close(writer, name)?;
667        }
668    }
669    Ok(())
670}
671
672fn is_scalar(v: &CanonicalValue) -> bool {
673    matches!(
674        v,
675        CanonicalValue::Null
676            | CanonicalValue::Bool(_)
677            | CanonicalValue::Number(_)
678            | CanonicalValue::String(_)
679    )
680}
681
682fn find_first_object_array(v: &CanonicalValue) -> Option<&Vec<CanonicalValue>> {
683    match v {
684        CanonicalValue::Array(a)
685            if !a.is_empty() && a.iter().all(|x| matches!(x, CanonicalValue::Object(_))) =>
686        {
687            Some(a)
688        }
689        CanonicalValue::Object(o) => {
690            for (_, v) in o {
691                if let Some(arr) = find_first_object_array(v) {
692                    return Some(arr);
693                }
694            }
695            None
696        }
697        _ => None,
698    }
699}
700
701fn cell_to_string(c: &calamine::Data) -> String {
702    use calamine::Data;
703    match c {
704        Data::Empty => String::new(),
705        Data::String(s) => s.clone(),
706        Data::Float(f) => f.to_string(),
707        Data::Int(i) => i.to_string(),
708        Data::Bool(b) => b.to_string(),
709        Data::DateTime(dt) => dt.to_string(),
710        Data::DateTimeIso(s) => s.clone(),
711        Data::DurationIso(s) => s.clone(),
712        Data::Error(e) => format!("{e:?}"),
713    }
714}