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)) => {
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 pub fn from_csv(input: &str) -> Result<Self, Box<dyn Error>> {
407 Self::from_delimited(input, b',')
408 }
409
410 pub fn to_csv(&self) -> Result<String, Box<dyn Error>> {
419 self.to_delimited(b',')
420 }
421
422 pub fn from_tsv(input: &str) -> Result<Self, Box<dyn Error>> {
431 Self::from_delimited(input, b'\t')
432 }
433
434 pub fn to_tsv(&self) -> Result<String, Box<dyn Error>> {
443 self.to_delimited(b'\t')
444 }
445
446 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 pub fn to_yaml(&self) -> Result<String, Box<dyn Error>> {
472 Ok(serde_saphyr::to_string(&self.to_json_value())?)
473 }
474
475 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 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}