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 Bytes(Vec<u8>),
31 Array(Vec<CanonicalValue>),
32 Object(Vec<(String, CanonicalValue)>),
33}
34
35type XmlStackFrame = (
36 String,
37 Vec<(String, String)>,
38 Vec<(String, CanonicalValue)>,
39 String,
40);
41
42impl CanonicalValue {
43 pub(crate) fn from_json_value(v: serde_json::Value) -> Self {
44 match v {
45 serde_json::Value::Null => Self::Null,
46 serde_json::Value::Bool(b) => Self::Bool(b),
47 serde_json::Value::Number(n) => Self::Number(n),
48 serde_json::Value::String(s) => Self::String(s),
49 serde_json::Value::Array(a) => {
50 Self::Array(a.into_iter().map(Self::from_json_value).collect())
51 }
52 serde_json::Value::Object(m) => {
53 if m.len() == 1 {
54 if let Some(serde_json::Value::String(b64)) = m.get("$binary") {
55 use base64::Engine;
56 if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(b64) {
57 return Self::Bytes(bytes);
58 }
59 }
60 }
61 Self::Object(
62 m.into_iter()
63 .map(|(k, v)| (k, Self::from_json_value(v)))
64 .collect(),
65 )
66 }
67 }
68 }
69
70 pub(crate) fn to_json_value(&self) -> serde_json::Value {
71 match self {
72 Self::Null => serde_json::Value::Null,
73 Self::Bool(b) => serde_json::Value::Bool(*b),
74 Self::Number(n) => serde_json::Value::Number(n.clone()),
75 Self::String(s) => serde_json::Value::String(s.clone()),
76 Self::Bytes(b) => {
77 use base64::Engine;
78 let mut map = serde_json::Map::new();
79 map.insert(
80 "$binary".to_string(),
81 serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(b)),
82 );
83 serde_json::Value::Object(map)
84 }
85 Self::Array(a) => {
86 serde_json::Value::Array(a.iter().map(|v| v.to_json_value()).collect())
87 }
88 Self::Object(o) => {
89 let mut map = serde_json::Map::new();
90 for (k, v) in o {
91 map.insert(k.clone(), v.to_json_value());
92 }
93 serde_json::Value::Object(map)
94 }
95 }
96 }
97
98 pub fn from_json(input: &str) -> Result<Self, Box<dyn Error>> {
109 Ok(Self::from_json_value(serde_json::from_str(input)?))
110 }
111
112 pub fn to_json(&self) -> Result<String, Box<dyn Error>> {
121 Ok(serde_json::to_string(&self.to_json_value())?)
122 }
123
124 fn from_delimited(input: &str, delim: u8) -> Result<Self, Box<dyn Error>> {
125 let input = input.strip_prefix('\u{feff}').unwrap_or(input);
126 let mut rdr = csv::ReaderBuilder::new()
127 .delimiter(delim)
128 .from_reader(Cursor::new(input));
129 let headers = rdr.headers()?.clone();
130 let mut seen = std::collections::HashSet::new();
131 for h in headers.iter() {
132 if !seen.insert(h) {
133 return Err(format!("duplicate header: {h}").into());
134 }
135 }
136 let mut rows = Vec::new();
137 for result in rdr.records() {
138 let record = result?;
139 let mut obj = Vec::with_capacity(headers.len());
140 for (h, v) in headers.iter().zip(record.iter()) {
141 obj.push((h.to_string(), Self::String(v.to_string())));
142 }
143 rows.push(Self::Object(obj));
144 }
145 Ok(Self::Array(rows))
146 }
147
148 fn to_delimited(&self, delim: u8) -> Result<String, Box<dyn Error>> {
149 let rows = self.as_tabular()?;
150 if rows.is_empty() {
151 return Ok(String::new());
152 }
153 let mut headers: Vec<String> = Vec::new();
154 for row in &rows {
155 let obj = row.as_object()?;
156 for (k, _) in obj {
157 if !headers.contains(k) {
158 headers.push(k.clone());
159 }
160 }
161 }
162 let mut wtr = csv::WriterBuilder::new()
163 .delimiter(delim)
164 .from_writer(vec![]);
165 wtr.write_record(&headers)?;
166 for row in &rows {
167 let obj = row.as_object()?;
168 let mut record = Vec::with_capacity(headers.len());
169 for h in &headers {
170 let cell = match obj.iter().find(|(k, _)| k == h) {
171 Some((_, Self::Null)) | None => String::new(),
172 Some((_, Self::Bool(b))) => b.to_string(),
173 Some((_, Self::Number(n))) => n.to_string(),
174 Some((_, Self::String(s))) => s.clone(),
175 Some((_, Self::Bytes(b))) => {
176 use base64::Engine;
177 base64::engine::general_purpose::STANDARD.encode(b)
178 }
179 Some((_, Self::Array(_))) | Some((_, Self::Object(_))) => {
180 return Err(format!(
181 "nested value at key {h} cannot be represented in tabular output"
182 )
183 .into());
184 }
185 };
186 record.push(cell);
187 }
188 wtr.write_record(&record)?;
189 }
190 let data = wtr.into_inner()?;
191 Ok(String::from_utf8(data)?)
192 }
193
194 pub fn from_toml(input: &str) -> Result<Self, Box<dyn Error>> {
195 let value: toml::Value = toml::from_str(input)?;
196 Ok(Self::from_toml_value(value))
197 }
198
199 fn from_toml_value(v: toml::Value) -> Self {
200 match v {
201 toml::Value::String(s) => Self::String(s),
202 toml::Value::Integer(i) => Self::Number(serde_json::Number::from(i)),
203 toml::Value::Float(f) => match serde_json::Number::from_f64(f) {
204 Some(n) => Self::Number(n),
205 None => Self::String(f.to_string()),
206 },
207 toml::Value::Boolean(b) => Self::Bool(b),
208 toml::Value::Datetime(dt) => Self::String(dt.to_string()),
209 toml::Value::Array(a) => {
210 Self::Array(a.into_iter().map(Self::from_toml_value).collect())
211 }
212 toml::Value::Table(t) => Self::Object(
213 t.into_iter()
214 .map(|(k, v)| (k, Self::from_toml_value(v)))
215 .collect(),
216 ),
217 }
218 }
219
220 pub fn to_toml(&self) -> Result<String, Box<dyn Error>> {
221 let value = match self {
222 Self::Array(_) => {
223 let mut table = toml::map::Map::new();
224 table.insert("item".to_string(), self.to_toml_value()?);
225 toml::Value::Table(table)
226 }
227 Self::Object(_) => self.to_toml_value()?,
228 _ => {
229 let mut table = toml::map::Map::new();
230 table.insert("value".to_string(), self.to_toml_value()?);
231 toml::Value::Table(table)
232 }
233 };
234 Ok(toml::to_string_pretty(&value)?)
235 }
236
237 fn to_toml_value(&self) -> Result<toml::Value, Box<dyn Error>> {
238 Ok(match self {
239 Self::Null => return Err("TOML has no null type".into()),
240 Self::Bool(b) => toml::Value::Boolean(*b),
241 Self::Number(n) => {
242 if let Some(i) = n.as_i64() {
243 toml::Value::Integer(i)
244 } else if let Some(u) = n.as_u64() {
245 if u <= i64::MAX as u64 {
246 toml::Value::Integer(u as i64)
247 } else {
248 return Err(format!("TOML integer out of range: {u}").into());
249 }
250 } else if let Some(f) = n.as_f64() {
251 toml::Value::Float(f)
252 } else {
253 return Err(format!("cannot represent number {n} in TOML").into());
254 }
255 }
256 Self::String(s) => toml::Value::String(s.clone()),
257 Self::Bytes(b) => {
258 use base64::Engine;
259 toml::Value::String(base64::engine::general_purpose::STANDARD.encode(b))
260 }
261 Self::Array(a) => {
262 let mut items = Vec::with_capacity(a.len());
263 for item in a {
264 if let Self::Null = item {
265 return Err("TOML arrays cannot contain null".into());
266 }
267 items.push(item.to_toml_value()?);
268 }
269 toml::Value::Array(items)
270 }
271 Self::Object(o) => {
272 let mut table = toml::map::Map::new();
273 for (k, v) in o {
274 if let Self::Null = v {
275 continue;
276 }
277 table.insert(k.clone(), v.to_toml_value()?);
278 }
279 toml::Value::Table(table)
280 }
281 })
282 }
283
284 pub fn from_xlsx_workbook(bytes: &[u8]) -> Result<Vec<(String, Self)>, Box<dyn Error>> {
285 use calamine::{Reader, Xlsx};
286 let cursor = Cursor::new(bytes.to_vec());
287 let mut workbook: Xlsx<_> = Xlsx::new(cursor)?;
288 let names = workbook.sheet_names().to_owned();
289 let mut out = Vec::with_capacity(names.len());
290 for name in names {
291 let range = workbook
292 .worksheet_range(&name)
293 .map_err(|e| format!("failed to read sheet {name}: {e:?}"))?;
294 out.push((name, Self::from_calamine_range(&range)?));
295 }
296 Ok(out)
297 }
298
299 pub(crate) fn from_calamine_range(
300 range: &calamine::Range<calamine::Data>,
301 ) -> Result<Self, Box<dyn Error>> {
302 use calamine::Data;
303 let mut rows = range.rows();
304 let header = match rows.next() {
305 Some(h) => h.iter().map(cell_to_string).collect::<Vec<String>>(),
306 None => return Ok(Self::Array(Vec::new())),
307 };
308 let mut records = Vec::new();
309 for row in rows {
310 if row.iter().all(|c| matches!(c, Data::Empty)) {
311 continue;
312 }
313 let mut obj = Vec::with_capacity(header.len());
314 for (i, h) in header.iter().enumerate() {
315 let val = match row.get(i) {
316 Some(Data::Empty) | None => Self::Null,
317 Some(Data::String(s)) => Self::String(s.clone()),
318 Some(Data::Float(f)) => {
319 if f.is_finite() && f.fract() == 0.0 && f.abs() < i64::MAX as f64 {
320 Self::Number(serde_json::Number::from(*f as i64))
321 } else {
322 match serde_json::Number::from_f64(*f) {
323 Some(n) => Self::Number(n),
324 None => Self::String(f.to_string()),
325 }
326 }
327 }
328 Some(Data::Int(i)) => Self::Number(serde_json::Number::from(*i)),
329 Some(Data::Bool(b)) => Self::Bool(*b),
330 Some(Data::DateTime(dt)) => Self::String(dt.to_string()),
331 Some(Data::DateTimeIso(s)) => Self::String(s.clone()),
332 Some(Data::DurationIso(s)) => Self::String(s.clone()),
333 Some(Data::Error(e)) => Self::String(format!("{e:?}")),
334 };
335 obj.push((h.clone(), val));
336 }
337 records.push(Self::Object(obj));
338 }
339 Ok(Self::Array(records))
340 }
341
342 pub fn to_xlsx_workbook(sheets: &[(String, Self)]) -> Result<Vec<u8>, Box<dyn Error>> {
343 use rust_xlsxwriter::Workbook;
344 let mut wb = Workbook::new();
345 for (name, value) in sheets {
346 let ws = wb.add_worksheet();
347 ws.set_name(name.as_str())?;
348 let rows = value.as_tabular()?;
349 if rows.is_empty() {
350 continue;
351 }
352 let mut headers: Vec<String> = Vec::new();
353 for row in &rows {
354 let obj = row.as_object()?;
355 for (k, _) in obj {
356 if !headers.contains(k) {
357 headers.push(k.clone());
358 }
359 }
360 }
361 for (col, h) in headers.iter().enumerate() {
362 ws.write_string(0, col as u16, h)?;
363 }
364 for (i, row) in rows.iter().enumerate() {
365 let obj = row.as_object()?;
366 for (col, h) in headers.iter().enumerate() {
367 let row_idx = (i + 1) as u32;
368 let col_idx = col as u16;
369 match obj.iter().find(|(k, _)| k == h) {
370 Some((_, Self::String(s))) => {
371 ws.write_string(row_idx, col_idx, s)?;
372 }
373 Some((_, Self::Number(n))) => {
374 if let Some(f) = n.as_f64() {
375 ws.write_number(row_idx, col_idx, f)?;
376 } else {
377 ws.write_string(row_idx, col_idx, n.to_string())?;
378 }
379 }
380 Some((_, Self::Bool(b))) => {
381 ws.write_boolean(row_idx, col_idx, *b)?;
382 }
383 Some((_, Self::Null)) | None => {}
384 Some((_, Self::Bytes(b))) => {
385 use base64::Engine;
386 let s = base64::engine::general_purpose::STANDARD.encode(b);
387 ws.write_string(row_idx, col_idx, &s)?;
388 }
389 Some((_, Self::Array(_))) | Some((_, Self::Object(_))) => {
390 return Err(format!(
391 "nested value at key {h} cannot be represented in XLSX"
392 )
393 .into());
394 }
395 }
396 }
397 }
398 }
399 Ok(wb.save_to_buffer()?)
400 }
401
402 pub fn from_sql_workbook(
403 input: &str,
404 dialect: crate::SqlDialect,
405 ) -> Result<Vec<(String, Self)>, Box<dyn Error>> {
406 use sqlparser::ast::{
407 Expr, SetExpr, Statement, TableObject, Value as SqlValue, ValueWithSpan, Values,
408 };
409 use sqlparser::dialect::{GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
410 use sqlparser::parser::Parser;
411
412 let statements = match dialect {
413 crate::SqlDialect::Generic => Parser::parse_sql(&GenericDialect {}, input)?,
414 crate::SqlDialect::PostgreSql => Parser::parse_sql(&PostgreSqlDialect {}, input)?,
415 crate::SqlDialect::Sqlite => Parser::parse_sql(&SQLiteDialect {}, input)?,
416 crate::SqlDialect::MySql => Parser::parse_sql(&MySqlDialect {}, input)?,
417 };
418
419 let mut schemas: std::collections::HashMap<String, Vec<String>> =
420 std::collections::HashMap::new();
421 let mut tables: Vec<SqlTable> = Vec::new();
422
423 for statement in statements {
424 match statement {
425 Statement::CreateTable(create) => {
426 let name = clean_ident(&create.name.to_string());
427 let cols: Vec<String> = create
428 .columns
429 .iter()
430 .map(|c| clean_ident(&c.name.to_string()))
431 .collect();
432 schemas.insert(name, cols);
433 }
434 Statement::Insert(insert) => {
435 let table = match &insert.table {
436 TableObject::TableName(n) => clean_ident(&n.to_string()),
437 _ => {
438 return Err(
439 "INSERT into table functions or queries is not supported".into()
440 );
441 }
442 };
443 let source = insert
444 .source
445 .as_ref()
446 .ok_or("INSERT without a source is not supported")?;
447 let rows = match source.body.as_ref() {
448 SetExpr::Values(Values { rows, .. }) => rows,
449 _ => {
450 return Err(
451 "only INSERT ... VALUES is supported for data extraction".into()
452 );
453 }
454 };
455 let explicit_cols: Vec<String> = insert
456 .columns
457 .iter()
458 .map(|c| clean_ident(&c.to_string()))
459 .collect();
460 let cols = if explicit_cols.is_empty() {
461 schemas.get(&table).cloned().unwrap_or_else(|| {
462 (0..rows.first().map(|r| r.len()).unwrap_or(0))
463 .map(|i| format!("column_{}", i + 1))
464 .collect()
465 })
466 } else {
467 explicit_cols
468 };
469 let slot = match tables.iter_mut().find(|(n, _)| n == &table) {
470 Some((_, rows)) => rows,
471 None => {
472 tables.push((table.clone(), Vec::new()));
473 &mut tables.last_mut().expect("just pushed").1
474 }
475 };
476 for row in rows {
477 let mut obj = Vec::with_capacity(cols.len());
478 for (i, expr) in row.iter().enumerate() {
479 let key = cols
480 .get(i)
481 .cloned()
482 .unwrap_or_else(|| format!("column_{}", i + 1));
483 let value = match expr {
484 Expr::Value(ValueWithSpan { value, .. }) => match value {
485 SqlValue::Number(n, _) => {
486 if let Ok(i) = n.parse::<i64>() {
487 Self::Number(serde_json::Number::from(i))
488 } else if let Ok(f) = n.parse::<f64>() {
489 match serde_json::Number::from_f64(f) {
490 Some(n) => Self::Number(n),
491 None => Self::String(n.clone()),
492 }
493 } else {
494 Self::String(n.clone())
495 }
496 }
497 SqlValue::SingleQuotedString(s)
498 | SqlValue::DoubleQuotedString(s)
499 | SqlValue::EscapedStringLiteral(s) => Self::String(s.clone()),
500 SqlValue::Boolean(b) => Self::Bool(*b),
501 SqlValue::Null => Self::Null,
502 other => Self::String(other.to_string()),
503 },
504 Expr::Identifier(id) => Self::String(id.value.clone()),
505 other => Self::String(other.to_string()),
506 };
507 obj.push((key, value));
508 }
509 slot.push(obj);
510 }
511 }
512 _ => {}
513 }
514 }
515
516 if tables.is_empty() {
517 return Err("no INSERT statements with literal VALUES found".into());
518 }
519
520 Ok(tables
521 .into_iter()
522 .map(|(name, rows)| {
523 let records = rows.into_iter().map(Self::Object).collect();
524 (name, Self::Array(records))
525 })
526 .collect())
527 }
528
529 pub fn to_sql_workbook(sheets: &[(String, Self)]) -> Result<String, Box<dyn Error>> {
530 let mut out = String::new();
531 for (table, value) in sheets {
532 out.push_str(&value.to_sql_with_table(table)?);
533 }
534 Ok(out)
535 }
536
537 pub fn to_sql_with_table(&self, table: &str) -> Result<String, Box<dyn Error>> {
538 let rows = self.as_tabular()?;
539 if rows.is_empty() {
540 return Ok(String::new());
541 }
542 let mut headers: Vec<String> = Vec::new();
543 for row in &rows {
544 let obj = row.as_object()?;
545 for (k, _) in obj {
546 if !headers.contains(k) {
547 headers.push(k.clone());
548 }
549 }
550 }
551 let cols = headers
552 .iter()
553 .map(|h| quote_ident(h))
554 .collect::<Vec<_>>()
555 .join(", ");
556 let mut out = String::new();
557 for row in &rows {
558 let obj = row.as_object()?;
559 let mut values = Vec::with_capacity(headers.len());
560 for h in &headers {
561 let cell = match obj.iter().find(|(k, _)| k == h) {
562 Some((_, Self::Null)) | None => "NULL".to_string(),
563 Some((_, Self::Bool(b))) => b.to_string().to_uppercase(),
564 Some((_, Self::Number(n))) => n.to_string(),
565 Some((_, Self::String(s))) => format!("'{}'", s.replace('\'', "''")),
566 Some((_, Self::Bytes(b))) => {
567 format!(
568 "X'{}'",
569 b.iter()
570 .map(|byte| format!("{byte:02x}"))
571 .collect::<String>()
572 )
573 }
574 Some((_, Self::Array(_))) | Some((_, Self::Object(_))) => {
575 return Err(format!(
576 "nested value at key {h} cannot be represented in SQL"
577 )
578 .into());
579 }
580 };
581 values.push(cell);
582 }
583 out.push_str(&format!(
584 "INSERT INTO {} ({}) VALUES ({});\n",
585 quote_ident(table),
586 cols,
587 values.join(", ")
588 ));
589 }
590 Ok(out)
591 }
592
593 fn as_object(&self) -> Result<&Vec<(String, CanonicalValue)>, Box<dyn Error>> {
594 match self {
595 Self::Object(o) => Ok(o),
596 _ => Err("expected object".into()),
597 }
598 }
599
600 pub(crate) fn as_tabular(&self) -> Result<Vec<&CanonicalValue>, Box<dyn Error>> {
601 if let Some(arr) = find_first_object_array(self) {
602 return Ok(arr.iter().collect());
603 }
604 let mut current = self;
605 while let Self::Object(o) = current {
606 if o.len() != 1 {
607 break;
608 }
609 match &o[0].1 {
610 Self::Object(_) => current = &o[0].1,
611 _ => break,
612 }
613 }
614 if let Self::Object(o) = current {
615 if o.iter().all(|(_, v)| is_scalar(v)) {
616 return Ok(vec![current]);
617 }
618 }
619 Err("cannot determine tabular structure".into())
620 }
621
622 pub fn from_csv(input: &str) -> Result<Self, Box<dyn Error>> {
631 Self::from_delimited(input, b',')
632 }
633
634 pub fn to_csv(&self) -> Result<String, Box<dyn Error>> {
643 self.to_delimited(b',')
644 }
645
646 pub fn from_tsv(input: &str) -> Result<Self, Box<dyn Error>> {
655 Self::from_delimited(input, b'\t')
656 }
657
658 pub fn to_tsv(&self) -> Result<String, Box<dyn Error>> {
667 self.to_delimited(b'\t')
668 }
669
670 pub fn from_yaml(input: &str) -> Result<Self, Box<dyn Error>> {
679 let opts = serde_saphyr::options! {
680 strict_booleans: true,
681 ignore_binary_tag_for_string: true,
682 };
683 let v: serde_json::Value = serde_saphyr::from_str_with_options(input, opts)?;
684 Ok(Self::from_json_value(v))
685 }
686
687 pub fn to_yaml(&self) -> Result<String, Box<dyn Error>> {
696 Ok(serde_saphyr::to_string(&self.to_json_value())?)
697 }
698
699 pub fn from_xml(input: &str) -> Result<Self, Box<dyn Error>> {
712 use quick_xml::events::Event;
713 use quick_xml::Reader;
714
715 let mut reader = Reader::from_str(input);
716 reader.config_mut().trim_text(true);
717 let mut stack: Vec<XmlStackFrame> = Vec::new();
718 let mut root: Option<(String, CanonicalValue)> = None;
719
720 loop {
721 match reader.read_event() {
722 Ok(Event::Start(e)) => {
723 let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
724 let mut attrs = Vec::new();
725 for a in e.attributes() {
726 let a = a?;
727 attrs.push((
728 String::from_utf8_lossy(a.key.as_ref()).to_string(),
729 a.unescape_value()?.to_string(),
730 ));
731 }
732 stack.push((name, attrs, Vec::new(), String::new()));
733 }
734 Ok(Event::Text(e)) => {
735 if let Some(top) = stack.last_mut() {
736 top.3.push_str(&e.unescape()?);
737 }
738 }
739 Ok(Event::End(_)) => {
740 let (name, attributes, children, text) = stack.pop().ok_or("unbalanced XML")?;
741 let trimmed = text.trim().to_string();
742 let value = if attributes.is_empty() && children.is_empty() {
743 Self::String(trimmed)
744 } else {
745 let mut obj: Vec<(String, CanonicalValue)> = Vec::new();
746 for (k, v) in attributes {
747 obj.push((format!("@{k}"), Self::String(v)));
748 }
749 if !trimmed.is_empty() {
750 obj.push(("#text".to_string(), Self::String(trimmed)));
751 }
752 for (cname, cval) in children {
753 if let Some(slot) = obj.iter_mut().find(|(k, _)| k == &cname) {
754 match &mut slot.1 {
755 Self::Array(a) => a.push(cval),
756 _ => {
757 let old = slot.1.clone();
758 slot.1 = Self::Array(vec![old, cval]);
759 }
760 }
761 } else {
762 obj.push((cname, cval));
763 }
764 }
765 Self::Object(obj)
766 };
767 if let Some(parent) = stack.last_mut() {
768 parent.2.push((name, value));
769 } else {
770 root = Some((name, value));
771 }
772 }
773 Ok(Event::Eof) => break,
774 Ok(_) => {}
775 Err(e) => return Err(e.into()),
776 }
777 }
778
779 let (root_name, root_value) = root.ok_or("empty XML")?;
780 Ok(Self::Object(vec![(root_name, root_value)]))
781 }
782
783 pub fn to_xml(&self) -> Result<String, Box<dyn Error>> {
794 let mut writer = quick_xml::Writer::new_with_indent(Vec::new(), b' ', 2);
795 match self {
796 Self::Object(o) if o.len() == 1 && !o[0].0.starts_with('@') && o[0].0 != "#text" => {
797 write_element(&mut writer, &o[0].0, &o[0].1)?;
798 }
799 Self::Array(a) => {
800 write_open(&mut writer, "root", &[])?;
801 for item in a {
802 write_element(&mut writer, "item", item)?;
803 }
804 write_close(&mut writer, "root")?;
805 }
806 _ => write_element(&mut writer, "root", self)?,
807 }
808 Ok(String::from_utf8(writer.into_inner())?)
809 }
810}
811
812fn write_open<W: std::io::Write>(
813 writer: &mut quick_xml::Writer<W>,
814 name: &str,
815 attrs: &[(String, String)],
816) -> Result<(), Box<dyn Error>> {
817 use quick_xml::events::{BytesStart, Event};
818 let mut start = BytesStart::new(name);
819 for (k, v) in attrs {
820 start.push_attribute((k.as_str(), v.as_str()));
821 }
822 writer.write_event(Event::Start(start))?;
823 Ok(())
824}
825
826fn write_close<W: std::io::Write>(
827 writer: &mut quick_xml::Writer<W>,
828 name: &str,
829) -> Result<(), Box<dyn Error>> {
830 use quick_xml::events::{BytesEnd, Event};
831 writer.write_event(Event::End(BytesEnd::new(name)))?;
832 Ok(())
833}
834
835fn write_element<W: std::io::Write>(
836 writer: &mut quick_xml::Writer<W>,
837 name: &str,
838 value: &CanonicalValue,
839) -> Result<(), Box<dyn Error>> {
840 use quick_xml::events::{BytesText, Event};
841
842 match value {
843 CanonicalValue::Array(items) => {
844 for item in items {
845 write_element(writer, name, item)?;
846 }
847 }
848 CanonicalValue::Object(o) => {
849 let attrs: Vec<(String, String)> = o
850 .iter()
851 .filter_map(|(k, v)| {
852 k.strip_prefix('@').and_then(|attr| match v {
853 CanonicalValue::String(s) => Some((attr.to_string(), s.clone())),
854 _ => None,
855 })
856 })
857 .collect();
858 write_open(writer, name, &attrs)?;
859 for (k, v) in o {
860 if k.starts_with('@') {
861 continue;
862 }
863 if k == "#text" {
864 if let CanonicalValue::String(s) = v {
865 writer.write_event(Event::Text(BytesText::new(s)))?;
866 }
867 continue;
868 }
869 write_element(writer, k, v)?;
870 }
871 write_close(writer, name)?;
872 }
873 CanonicalValue::String(s) => {
874 write_open(writer, name, &[])?;
875 writer.write_event(Event::Text(BytesText::new(s)))?;
876 write_close(writer, name)?;
877 }
878 CanonicalValue::Bytes(b) => {
879 use base64::Engine;
880 let s = base64::engine::general_purpose::STANDARD.encode(b);
881 write_open(writer, name, &[])?;
882 writer.write_event(Event::Text(BytesText::new(&s)))?;
883 write_close(writer, name)?;
884 }
885 CanonicalValue::Number(n) => {
886 write_open(writer, name, &[])?;
887 writer.write_event(Event::Text(BytesText::new(&n.to_string())))?;
888 write_close(writer, name)?;
889 }
890 CanonicalValue::Bool(b) => {
891 write_open(writer, name, &[])?;
892 writer.write_event(Event::Text(BytesText::new(&b.to_string())))?;
893 write_close(writer, name)?;
894 }
895 CanonicalValue::Null => {
896 write_open(writer, name, &[])?;
897 write_close(writer, name)?;
898 }
899 }
900 Ok(())
901}
902
903type SqlRow = Vec<(String, CanonicalValue)>;
904type SqlTable = (String, Vec<SqlRow>);
905
906impl CanonicalValue {
907 pub fn from_ods(bytes: &[u8]) -> Result<Self, Box<dyn Error>> {
918 use calamine::{Ods, Reader};
919 let cursor = Cursor::new(bytes.to_vec());
920 let mut workbook: Ods<_> = Ods::new(cursor)?;
921 let names = workbook.sheet_names().to_owned();
922 let mut out: Vec<(String, CanonicalValue)> = Vec::with_capacity(names.len());
923 for name in names {
924 let range = workbook
925 .worksheet_range(&name)
926 .map_err(|e| format!("failed to read sheet {name}: {e:?}"))?;
927 out.push((name, Self::from_calamine_range(&range)?));
928 }
929 if out.len() == 1 {
930 Ok(out.remove(0).1)
931 } else {
932 Ok(Self::Object(out))
933 }
934 }
935}
936
937pub fn to_ods_workbook(sheets: &[(String, CanonicalValue)]) -> Result<Vec<u8>, Box<dyn Error>> {
947 use std::io::Write as IoWrite;
948 use zip::write::SimpleFileOptions;
949 use zip::{CompressionMethod, ZipWriter};
950
951 let content = ods_content_xml(sheets)?;
952 let manifest = ods_manifest_xml();
953 let mimetype = "application/vnd.oasis.opendocument.spreadsheet";
954
955 let cursor = Cursor::new(Vec::new());
956 let mut zip = ZipWriter::new(cursor);
957 let stored = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
958 let deflated = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
959
960 zip.start_file("mimetype", stored)?;
961 zip.write_all(mimetype.as_bytes())?;
962
963 zip.start_file("META-INF/manifest.xml", deflated)?;
964 zip.write_all(manifest.as_bytes())?;
965
966 zip.start_file("content.xml", deflated)?;
967 zip.write_all(content.as_bytes())?;
968
969 let cursor = zip.finish()?;
970 Ok(cursor.into_inner())
971}
972
973fn ods_manifest_xml() -> String {
974 r#"<?xml version="1.0" encoding="UTF-8"?>
975<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">
976 <manifest:file-entry manifest:full-path="/" manifest:version="1.2" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
977 <manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
978</manifest:manifest>
979"#
980 .to_string()
981}
982
983fn ods_content_xml(sheets: &[(String, CanonicalValue)]) -> Result<String, Box<dyn Error>> {
984 use quick_xml::escape::escape;
985 let mut out = String::new();
986 out.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
987 out.push_str(
988 r#"<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.2">"#,
989 );
990 out.push_str("<office:body><office:spreadsheet>");
991
992 for (name, value) in sheets {
993 out.push_str(&format!("<table:table table:name=\"{}\">", escape(name)));
994 let rows = value.as_tabular()?;
995 let mut headers: Vec<String> = Vec::new();
996 for row in &rows {
997 if let CanonicalValue::Object(o) = row {
998 for (k, _) in o {
999 if !headers.contains(k) {
1000 headers.push(k.clone());
1001 }
1002 }
1003 }
1004 }
1005 out.push_str("<table:table-row>");
1006 for h in &headers {
1007 out.push_str("<table:table-cell office:value-type=\"string\">");
1008 out.push_str(&format!("<text:p>{}</text:p>", escape(h)));
1009 out.push_str("</table:table-cell>");
1010 }
1011 out.push_str("</table:table-row>");
1012
1013 for row in &rows {
1014 if let CanonicalValue::Object(o) = row {
1015 out.push_str("<table:table-row>");
1016 for h in &headers {
1017 match o.iter().find(|(k, _)| k == h).map(|(_, v)| v) {
1018 Some(CanonicalValue::String(s)) => {
1019 out.push_str("<table:table-cell office:value-type=\"string\">");
1020 out.push_str(&format!("<text:p>{}</text:p>", escape(s)));
1021 out.push_str("</table:table-cell>");
1022 }
1023 Some(CanonicalValue::Number(n)) => {
1024 out.push_str(&format!(
1025 "<table:table-cell office:value-type=\"float\" office:value=\"{}\">",
1026 n
1027 ));
1028 out.push_str(&format!("<text:p>{}</text:p>", n));
1029 out.push_str("</table:table-cell>");
1030 }
1031 Some(CanonicalValue::Bool(b)) => {
1032 out.push_str(&format!(
1033 "<table:table-cell office:value-type=\"boolean\" office:boolean-value=\"{}\">",
1034 b
1035 ));
1036 out.push_str(&format!(
1037 "<text:p>{}</text:p>",
1038 if *b { "TRUE" } else { "FALSE" }
1039 ));
1040 out.push_str("</table:table-cell>");
1041 }
1042 Some(CanonicalValue::Bytes(b)) => {
1043 use base64::Engine;
1044 let s = base64::engine::general_purpose::STANDARD.encode(b);
1045 out.push_str("<table:table-cell office:value-type=\"string\">");
1046 out.push_str(&format!("<text:p>{}</text:p>", s));
1047 out.push_str("</table:table-cell>");
1048 }
1049 Some(CanonicalValue::Null) | None => {
1050 out.push_str("<table:table-cell/>");
1051 }
1052 Some(CanonicalValue::Array(_)) | Some(CanonicalValue::Object(_)) => {
1053 return Err(format!(
1054 "nested value at key {h} cannot be represented in ODS"
1055 )
1056 .into());
1057 }
1058 }
1059 }
1060 out.push_str("</table:table-row>");
1061 }
1062 }
1063 out.push_str("</table:table>");
1064 }
1065
1066 out.push_str("</office:spreadsheet></office:body></office:document-content>");
1067 Ok(out)
1068}
1069
1070fn is_scalar(v: &CanonicalValue) -> bool {
1071 matches!(
1072 v,
1073 CanonicalValue::Null
1074 | CanonicalValue::Bool(_)
1075 | CanonicalValue::Number(_)
1076 | CanonicalValue::String(_)
1077 )
1078}
1079
1080fn find_first_object_array(v: &CanonicalValue) -> Option<&Vec<CanonicalValue>> {
1081 match v {
1082 CanonicalValue::Array(a)
1083 if !a.is_empty() && a.iter().all(|x| matches!(x, CanonicalValue::Object(_))) =>
1084 {
1085 Some(a)
1086 }
1087 CanonicalValue::Object(o) => {
1088 for (_, v) in o {
1089 if let Some(arr) = find_first_object_array(v) {
1090 return Some(arr);
1091 }
1092 }
1093 None
1094 }
1095 _ => None,
1096 }
1097}
1098
1099fn cell_to_string(c: &calamine::Data) -> String {
1100 use calamine::Data;
1101 match c {
1102 Data::Empty => String::new(),
1103 Data::String(s) => s.clone(),
1104 Data::Float(f) => f.to_string(),
1105 Data::Int(i) => i.to_string(),
1106 Data::Bool(b) => b.to_string(),
1107 Data::DateTime(dt) => dt.to_string(),
1108 Data::DateTimeIso(s) => s.clone(),
1109 Data::DurationIso(s) => s.clone(),
1110 Data::Error(e) => format!("{e:?}"),
1111 }
1112}
1113
1114fn clean_ident(s: &str) -> String {
1115 s.trim_matches('"').trim_matches('`').to_string()
1116}
1117
1118fn quote_ident(s: &str) -> String {
1119 format!("\"{}\"", s.replace('"', "\"\""))
1120}
1121
1122impl CanonicalValue {
1123 pub fn from_ndjson(input: &str) -> Result<Self, Box<dyn std::error::Error>> {
1138 let mut values = Vec::new();
1139 for line in input.lines() {
1140 let trimmed = line.trim();
1141 if trimmed.is_empty() {
1142 continue;
1143 }
1144 let v: serde_json::Value = serde_json::from_str(trimmed)?;
1145 values.push(Self::from_json_value(v));
1146 }
1147 Ok(Self::Array(values))
1148 }
1149
1150 pub fn to_ndjson(&self) -> Result<String, Box<dyn std::error::Error>> {
1165 let mut out = String::new();
1166 match self {
1167 Self::Array(items) => {
1168 for item in items {
1169 out.push_str(&serde_json::to_string(&item.to_json_value())?);
1170 out.push('\n');
1171 }
1172 }
1173 other => {
1174 out.push_str(&serde_json::to_string(&other.to_json_value())?);
1175 out.push('\n');
1176 }
1177 }
1178 Ok(out)
1179 }
1180}