1use std::error::Error;
2use std::io::Cursor;
3
4#[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 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 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 pub fn from_csv(input: &str) -> Result<Self, Box<dyn Error>> {
401 Self::from_delimited(input, b',')
402 }
403
404 pub fn to_csv(&self) -> Result<String, Box<dyn Error>> {
413 self.to_delimited(b',')
414 }
415
416 pub fn from_tsv(input: &str) -> Result<Self, Box<dyn Error>> {
425 Self::from_delimited(input, b'\t')
426 }
427
428 pub fn to_tsv(&self) -> Result<String, Box<dyn Error>> {
437 self.to_delimited(b'\t')
438 }
439
440 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 pub fn to_yaml(&self) -> Result<String, Box<dyn Error>> {
466 Ok(serde_saphyr::to_string(&self.to_json_value())?)
467 }
468
469 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 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}