Skip to main content

sheeter_engine/
inspect.rs

1use crate::{CanonicalValue, Format, SqlDialect};
2use std::error::Error;
3
4/// Prints a structural summary of an input document.
5///
6/// Reports the format, the number of top-level documents (sheets), and for
7/// each sheet the number of rows and columns with inferred types.
8///
9/// # Examples
10///
11/// ```
12/// use sheeter_engine::{inspect, Format};
13///
14/// let report = inspect(b"name,age\nAlice,30\nBob,25\n", Format::Csv).unwrap();
15/// assert!(report.contains("rows: 2"));
16/// assert!(report.contains("name"));
17/// ```
18pub fn inspect(input: &[u8], format: Format) -> Result<String, Box<dyn Error>> {
19    let sheets = load_sheets(input, format)?;
20    let mut out = String::new();
21    out.push_str(&format!("format: {}\n", format_name(format)));
22    out.push_str(&format!("documents: {}\n", sheets.len()));
23    for (name, value) in &sheets {
24        describe_sheet(&mut out, name, value);
25    }
26    Ok(out)
27}
28
29fn format_name(f: Format) -> &'static str {
30    match f {
31        Format::Csv => "csv",
32        Format::Tsv => "tsv",
33        Format::Json => "json",
34        Format::Yaml => "yaml",
35        Format::Xml => "xml",
36        Format::Toml => "toml",
37        Format::Xlsx => "xlsx",
38        Format::Sql => "sql",
39        Format::Ndjson => "ndjson",
40        Format::Ods => "ods",
41        Format::Parquet => "parquet",
42    }
43}
44
45fn load_sheets(
46    input: &[u8],
47    format: Format,
48) -> Result<Vec<(String, CanonicalValue)>, Box<dyn Error>> {
49    match format {
50        Format::Xlsx => CanonicalValue::from_xlsx_workbook(input),
51        Format::Sql => {
52            let text = std::str::from_utf8(input)?;
53            CanonicalValue::from_sql_workbook(text, SqlDialect::Generic)
54        }
55        Format::Ndjson => {
56            let text = std::str::from_utf8(input)?;
57            Ok(vec![(
58                "Sheet1".to_string(),
59                CanonicalValue::from_ndjson(text)?,
60            )])
61        }
62        Format::Parquet => Ok(vec![("Sheet1".to_string(), crate::from_parquet(input)?)]),
63        _ => {
64            let text = std::str::from_utf8(input)?;
65            let hub = match format {
66                Format::Csv => CanonicalValue::from_csv(text)?,
67                Format::Tsv => CanonicalValue::from_tsv(text)?,
68                Format::Json => CanonicalValue::from_json(text)?,
69                Format::Yaml => CanonicalValue::from_yaml(text)?,
70                Format::Xml => CanonicalValue::from_xml(text)?,
71                Format::Toml => CanonicalValue::from_toml(text)?,
72                Format::Sql | Format::Ndjson | Format::Xlsx | Format::Ods | Format::Parquet => {
73                    unreachable!()
74                }
75            };
76            Ok(vec![("Sheet1".to_string(), hub)])
77        }
78    }
79}
80
81fn describe_sheet(out: &mut String, name: &str, value: &CanonicalValue) {
82    out.push_str(&format!("  {name}:\n"));
83    match value {
84        CanonicalValue::Array(rows) => {
85            let cols = collect_headers(rows);
86            out.push_str(&format!("    rows: {}\n", rows.len()));
87            out.push_str(&format!("    columns: {}\n", cols.len()));
88            for (i, col) in cols.iter().enumerate() {
89                let ty = infer_type(rows, col);
90                out.push_str(&format!("      {}: {} ({})\n", i + 1, col, ty));
91            }
92        }
93        CanonicalValue::Object(o) => {
94            out.push_str(&format!("    fields: {}\n", o.len()));
95            for (k, v) in o {
96                out.push_str(&format!("      {k}: {}\n", infer_scalar(v)));
97            }
98        }
99        CanonicalValue::String(s) => {
100            out.push_str(&format!("    scalar: string ({} bytes)\n", s.len()));
101        }
102        CanonicalValue::Bytes(b) => {
103            out.push_str(&format!("    scalar: binary ({} bytes)\n", b.len()));
104        }
105        CanonicalValue::Number(n) => out.push_str(&format!("    scalar: number ({n})\n")),
106        CanonicalValue::Bool(b) => out.push_str(&format!("    scalar: bool ({b})\n")),
107        CanonicalValue::Null => out.push_str("    scalar: null\n"),
108    }
109}
110
111fn collect_headers(rows: &[CanonicalValue]) -> Vec<String> {
112    let mut out: Vec<String> = Vec::new();
113    for row in rows {
114        if let CanonicalValue::Object(o) = row {
115            for (k, _) in o {
116                if !out.contains(k) {
117                    out.push(k.clone());
118                }
119            }
120        }
121    }
122    out
123}
124
125fn infer_type(rows: &[CanonicalValue], col: &str) -> &'static str {
126    let mut has_int = false;
127    let mut has_float = false;
128    let mut has_str = false;
129    let mut has_bool = false;
130    let mut has_null = false;
131    for row in rows {
132        if let CanonicalValue::Object(o) = row {
133            match o.iter().find(|(k, _)| k == col).map(|(_, v)| v) {
134                Some(CanonicalValue::Number(n)) => {
135                    if n.is_i64() || n.is_u64() {
136                        has_int = true;
137                    } else {
138                        has_float = true;
139                    }
140                }
141                Some(CanonicalValue::String(_)) => has_str = true,
142                Some(CanonicalValue::Bool(_)) => has_bool = true,
143                Some(CanonicalValue::Null) | None => has_null = true,
144                _ => has_str = true,
145            }
146        }
147    }
148    match (has_int, has_float, has_str, has_bool, has_null) {
149        (true, false, false, false, _) => "integer",
150        (_, true, false, false, _) => "number",
151        (false, false, false, true, _) => "boolean",
152        (false, false, true, false, _) => "string",
153        (false, false, false, false, true) => "null",
154        _ => "mixed",
155    }
156}
157
158fn infer_scalar(v: &CanonicalValue) -> &'static str {
159    match v {
160        CanonicalValue::Null => "null",
161        CanonicalValue::Bool(_) => "boolean",
162        CanonicalValue::Number(n) if n.is_i64() || n.is_u64() => "integer",
163        CanonicalValue::Number(_) => "number",
164        CanonicalValue::String(_) => "string",
165        CanonicalValue::Bytes(_) => "binary",
166        CanonicalValue::Array(_) => "array",
167        CanonicalValue::Object(_) => "object",
168    }
169}