1use crate::{detect_content, detect_extension, CanonicalValue, Format, SqlDialect};
32use comfy_table::{presets, Cell, CellAlignment, ContentArrangement, Table};
33use std::error::Error;
34use std::path::Path;
35
36pub fn expand(path: &str) -> Result<Vec<(String, CanonicalValue)>, Box<dyn Error>> {
48 let bytes = std::fs::read(path)?;
49 let fmt = match detect_extension(Path::new(path)) {
50 Some(f) => f,
51 None => detect_content(&bytes)?,
52 };
53 let stem = Path::new(path)
54 .file_stem()
55 .and_then(|s| s.to_str())
56 .unwrap_or(path)
57 .to_string();
58
59 match fmt {
60 Format::Xlsx => {
61 let sheets = CanonicalValue::from_xlsx_workbook(&bytes)?;
62 Ok(sheets
63 .into_iter()
64 .map(|(name, v)| (format!("{stem} ({name})"), v))
65 .collect())
66 }
67 Format::Ods => {
68 let v = CanonicalValue::from_ods(&bytes)?;
69 match v {
70 CanonicalValue::Object(o) if o.len() > 1 => Ok(o
71 .into_iter()
72 .map(|(name, v)| (format!("{stem} ({name})"), v))
73 .collect()),
74 other => Ok(vec![(stem, other)]),
75 }
76 }
77 Format::Parquet => {
78 let v = crate::from_parquet(&bytes)?;
79 Ok(vec![(stem, v)])
80 }
81 other => {
82 let text = std::str::from_utf8(&bytes)?;
83 let hub = decode_text(other, text)?;
84 if let CanonicalValue::Object(o) = &hub {
85 if other == Format::Sql && o.len() > 1 {
86 let owned = hub.clone();
87 let CanonicalValue::Object(inner) = owned else {
88 unreachable!()
89 };
90 return Ok(inner
91 .into_iter()
92 .map(|(name, v)| (format!("{stem} ({name})"), v))
93 .collect());
94 }
95 }
96 Ok(vec![(stem, hub)])
97 }
98 }
99}
100
101fn decode_text(fmt: Format, text: &str) -> Result<CanonicalValue, Box<dyn Error>> {
102 match fmt {
103 Format::Csv => CanonicalValue::from_csv(text),
104 Format::Tsv => CanonicalValue::from_tsv(text),
105 Format::Json => CanonicalValue::from_json(text),
106 Format::Ndjson => CanonicalValue::from_ndjson(text),
107 Format::Yaml => CanonicalValue::from_yaml(text),
108 Format::Xml => CanonicalValue::from_xml(text),
109 Format::Toml => CanonicalValue::from_toml(text),
110 Format::Sql => {
111 let sheets = CanonicalValue::from_sql_workbook(text, SqlDialect::Generic)?;
112 if sheets.len() == 1 {
113 let mut it = sheets.into_iter();
114 Ok(it.next().unwrap().1)
115 } else {
116 Ok(CanonicalValue::Object(sheets))
117 }
118 }
119 Format::Xlsx | Format::Ods | Format::Parquet => {
120 Err(format!("{fmt:?} requires binary decode").into())
121 }
122 }
123}
124
125pub fn render(
140 sources: Vec<(String, CanonicalValue)>,
141 to_json: bool,
142) -> Result<String, Box<dyn Error>> {
143 let mut out = String::new();
144 if to_json {
145 if sources.len() == 1 {
146 out.push_str(&sources[0].1.to_json()?);
147 out.push('\n');
148 } else {
149 let mut map = serde_json::Map::new();
150 for (label, value) in &sources {
151 map.insert(label.clone(), value.to_json_value());
152 }
153 out.push_str(&serde_json::to_string(&map)?);
154 out.push('\n');
155 }
156 return Ok(out);
157 }
158 for (i, (label, value)) in sources.iter().enumerate() {
159 if i > 0 {
160 out.push('\n');
161 }
162 out.push_str(&format!("Output table No. {} — {}\n", i + 1, label));
163 out.push_str(&render_table(value)?);
164 out.push('\n');
165 }
166 Ok(out)
167}
168
169fn render_table(value: &CanonicalValue) -> Result<String, Box<dyn Error>> {
170 let rows: Vec<CanonicalValue> = match value {
171 CanonicalValue::Array(a) => a.clone(),
172 CanonicalValue::Object(o) => vec![CanonicalValue::Object(o.clone())],
173 other => {
174 let mut t = Table::new();
175 t.load_preset(presets::UTF8_FULL);
176 t.add_row(vec![cell_from(other)]);
177 return Ok(t.to_string());
178 }
179 };
180
181 let mut headers: Vec<String> = Vec::new();
182 for row in &rows {
183 if let CanonicalValue::Object(o) = row {
184 for (k, _) in o {
185 if !headers.contains(k) {
186 headers.push(k.clone());
187 }
188 }
189 }
190 }
191
192 if headers.is_empty() {
193 return Ok(String::new());
194 }
195
196 let mut table = Table::new();
197 table.load_preset(presets::UTF8_FULL);
198 table.set_content_arrangement(ContentArrangement::Dynamic);
199 table.set_header(headers.iter().map(Cell::new));
200
201 for row in &rows {
202 if let CanonicalValue::Object(o) = row {
203 let cells: Vec<Cell> = headers
204 .iter()
205 .map(|h| match o.iter().find(|(k, _)| k == h).map(|(_, v)| v) {
206 Some(v) => cell_from(v),
207 None => Cell::new(""),
208 })
209 .collect();
210 table.add_row(cells);
211 }
212 }
213
214 Ok(table.to_string())
215}
216
217fn cell_from(v: &CanonicalValue) -> Cell {
218 match v {
219 CanonicalValue::Null => Cell::new(""),
220 CanonicalValue::Bool(b) => Cell::new(b.to_string()),
221 CanonicalValue::Number(n) => Cell::new(n.to_string()).set_alignment(CellAlignment::Right),
222 CanonicalValue::String(s) => Cell::new(s),
223 CanonicalValue::Bytes(b) => Cell::new(format!("<{} bytes>", b.len())),
224 CanonicalValue::Array(_) | CanonicalValue::Object(_) => Cell::new("<nested>"),
225 }
226}