1use crate::{CanonicalValue, Format, SqlDialect};
2use std::error::Error;
3
4pub 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}