use crate::{detect_content, detect_extension, CanonicalValue, Format, SqlDialect};
use comfy_table::{presets, Cell, CellAlignment, ContentArrangement, Table};
use std::error::Error;
use std::path::Path;
pub fn expand(path: &str) -> Result<Vec<(String, CanonicalValue)>, Box<dyn Error>> {
let bytes = std::fs::read(path)?;
let fmt = match detect_extension(Path::new(path)) {
Some(f) => f,
None => detect_content(&bytes)?,
};
let stem = Path::new(path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(path)
.to_string();
match fmt {
Format::Xlsx => {
let sheets = CanonicalValue::from_xlsx_workbook(&bytes)?;
Ok(sheets
.into_iter()
.map(|(name, v)| (format!("{stem} ({name})"), v))
.collect())
}
Format::Ods => {
let v = CanonicalValue::from_ods(&bytes)?;
match v {
CanonicalValue::Object(o) if o.len() > 1 => Ok(o
.into_iter()
.map(|(name, v)| (format!("{stem} ({name})"), v))
.collect()),
other => Ok(vec![(stem, other)]),
}
}
other => {
let text = std::str::from_utf8(&bytes)?;
let hub = decode_text(other, text)?;
if let CanonicalValue::Object(o) = &hub {
if other == Format::Sql && o.len() > 1 {
let owned = hub.clone();
let CanonicalValue::Object(inner) = owned else {
unreachable!()
};
return Ok(inner
.into_iter()
.map(|(name, v)| (format!("{stem} ({name})"), v))
.collect());
}
}
Ok(vec![(stem, hub)])
}
}
}
fn decode_text(fmt: Format, text: &str) -> Result<CanonicalValue, Box<dyn Error>> {
match fmt {
Format::Csv => CanonicalValue::from_csv(text),
Format::Tsv => CanonicalValue::from_tsv(text),
Format::Json => CanonicalValue::from_json(text),
Format::Ndjson => CanonicalValue::from_ndjson(text),
Format::Yaml => CanonicalValue::from_yaml(text),
Format::Xml => CanonicalValue::from_xml(text),
Format::Toml => CanonicalValue::from_toml(text),
Format::Sql => {
let sheets = CanonicalValue::from_sql_workbook(text, SqlDialect::Generic)?;
if sheets.len() == 1 {
let mut it = sheets.into_iter();
Ok(it.next().unwrap().1)
} else {
Ok(CanonicalValue::Object(sheets))
}
}
Format::Xlsx | Format::Ods => Err(format!("{fmt:?} requires binary decode").into()),
}
}
pub fn render(
sources: Vec<(String, CanonicalValue)>,
to_json: bool,
) -> Result<String, Box<dyn Error>> {
let mut out = String::new();
if to_json {
if sources.len() == 1 {
out.push_str(&sources[0].1.to_json()?);
out.push('\n');
} else {
let mut map = serde_json::Map::new();
for (label, value) in &sources {
map.insert(label.clone(), value.to_json_value());
}
out.push_str(&serde_json::to_string(&map)?);
out.push('\n');
}
return Ok(out);
}
for (i, (label, value)) in sources.iter().enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&format!("Output table No. {} — {}\n", i + 1, label));
out.push_str(&render_table(value)?);
out.push('\n');
}
Ok(out)
}
fn render_table(value: &CanonicalValue) -> Result<String, Box<dyn Error>> {
let rows: Vec<CanonicalValue> = match value {
CanonicalValue::Array(a) => a.clone(),
CanonicalValue::Object(o) => vec![CanonicalValue::Object(o.clone())],
other => {
let mut t = Table::new();
t.load_preset(presets::UTF8_FULL);
t.add_row(vec![cell_from(other)]);
return Ok(t.to_string());
}
};
let mut headers: Vec<String> = Vec::new();
for row in &rows {
if let CanonicalValue::Object(o) = row {
for (k, _) in o {
if !headers.contains(k) {
headers.push(k.clone());
}
}
}
}
if headers.is_empty() {
return Ok(String::new());
}
let mut table = Table::new();
table.load_preset(presets::UTF8_FULL);
table.set_content_arrangement(ContentArrangement::Dynamic);
table.set_header(headers.iter().map(Cell::new));
for row in &rows {
if let CanonicalValue::Object(o) = row {
let cells: Vec<Cell> = headers
.iter()
.map(|h| match o.iter().find(|(k, _)| k == h).map(|(_, v)| v) {
Some(v) => cell_from(v),
None => Cell::new(""),
})
.collect();
table.add_row(cells);
}
}
Ok(table.to_string())
}
fn cell_from(v: &CanonicalValue) -> Cell {
match v {
CanonicalValue::Null => Cell::new(""),
CanonicalValue::Bool(b) => Cell::new(b.to_string()),
CanonicalValue::Number(n) => Cell::new(n.to_string()).set_alignment(CellAlignment::Right),
CanonicalValue::String(s) => Cell::new(s),
CanonicalValue::Bytes(b) => Cell::new(format!("<{} bytes>", b.len())),
CanonicalValue::Array(_) | CanonicalValue::Object(_) => Cell::new("<nested>"),
}
}