use crate::{CanonicalValue, Format, SqlDialect};
use std::error::Error;
pub fn inspect(input: &[u8], format: Format) -> Result<String, Box<dyn Error>> {
let sheets = load_sheets(input, format)?;
let mut out = String::new();
out.push_str(&format!("format: {}\n", format_name(format)));
out.push_str(&format!("documents: {}\n", sheets.len()));
for (name, value) in &sheets {
describe_sheet(&mut out, name, value);
}
Ok(out)
}
fn format_name(f: Format) -> &'static str {
match f {
Format::Csv => "csv",
Format::Tsv => "tsv",
Format::Json => "json",
Format::Yaml => "yaml",
Format::Xml => "xml",
Format::Toml => "toml",
Format::Xlsx => "xlsx",
Format::Sql => "sql",
}
}
fn load_sheets(
input: &[u8],
format: Format,
) -> Result<Vec<(String, CanonicalValue)>, Box<dyn Error>> {
match format {
Format::Xlsx => CanonicalValue::from_xlsx_workbook(input),
Format::Sql => {
let text = std::str::from_utf8(input)?;
CanonicalValue::from_sql_workbook(text, SqlDialect::Generic)
}
_ => {
let text = std::str::from_utf8(input)?;
let hub = match format {
Format::Csv => CanonicalValue::from_csv(text)?,
Format::Tsv => CanonicalValue::from_tsv(text)?,
Format::Json => CanonicalValue::from_json(text)?,
Format::Yaml => CanonicalValue::from_yaml(text)?,
Format::Xml => CanonicalValue::from_xml(text)?,
Format::Toml => CanonicalValue::from_toml(text)?,
Format::Xlsx | Format::Sql => unreachable!(),
};
Ok(vec![("Sheet1".to_string(), hub)])
}
}
}
fn describe_sheet(out: &mut String, name: &str, value: &CanonicalValue) {
out.push_str(&format!(" {name}:\n"));
match value {
CanonicalValue::Array(rows) => {
let cols = collect_headers(rows);
out.push_str(&format!(" rows: {}\n", rows.len()));
out.push_str(&format!(" columns: {}\n", cols.len()));
for (i, col) in cols.iter().enumerate() {
let ty = infer_type(rows, col);
out.push_str(&format!(" {}: {} ({})\n", i + 1, col, ty));
}
}
CanonicalValue::Object(o) => {
out.push_str(&format!(" fields: {}\n", o.len()));
for (k, v) in o {
out.push_str(&format!(" {k}: {}\n", infer_scalar(v)));
}
}
CanonicalValue::String(s) => {
out.push_str(&format!(" scalar: string ({} bytes)\n", s.len()));
}
CanonicalValue::Number(n) => out.push_str(&format!(" scalar: number ({n})\n")),
CanonicalValue::Bool(b) => out.push_str(&format!(" scalar: bool ({b})\n")),
CanonicalValue::Null => out.push_str(" scalar: null\n"),
}
}
fn collect_headers(rows: &[CanonicalValue]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for row in rows {
if let CanonicalValue::Object(o) = row {
for (k, _) in o {
if !out.contains(k) {
out.push(k.clone());
}
}
}
}
out
}
fn infer_type(rows: &[CanonicalValue], col: &str) -> &'static str {
let mut has_int = false;
let mut has_float = false;
let mut has_str = false;
let mut has_bool = false;
let mut has_null = false;
for row in rows {
if let CanonicalValue::Object(o) = row {
match o.iter().find(|(k, _)| k == col).map(|(_, v)| v) {
Some(CanonicalValue::Number(n)) => {
if n.is_i64() || n.is_u64() {
has_int = true;
} else {
has_float = true;
}
}
Some(CanonicalValue::String(_)) => has_str = true,
Some(CanonicalValue::Bool(_)) => has_bool = true,
Some(CanonicalValue::Null) | None => has_null = true,
_ => has_str = true,
}
}
}
match (has_int, has_float, has_str, has_bool, has_null) {
(true, false, false, false, _) => "integer",
(_, true, false, false, _) => "number",
(false, false, false, true, _) => "boolean",
(false, false, true, false, _) => "string",
(false, false, false, false, true) => "null",
_ => "mixed",
}
}
fn infer_scalar(v: &CanonicalValue) -> &'static str {
match v {
CanonicalValue::Null => "null",
CanonicalValue::Bool(_) => "boolean",
CanonicalValue::Number(n) if n.is_i64() || n.is_u64() => "integer",
CanonicalValue::Number(_) => "number",
CanonicalValue::String(_) => "string",
CanonicalValue::Array(_) => "array",
CanonicalValue::Object(_) => "object",
}
}