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 other => {
78 let text = std::str::from_utf8(&bytes)?;
79 let hub = decode_text(other, text)?;
80 if let CanonicalValue::Object(o) = &hub {
81 if other == Format::Sql && o.len() > 1 {
82 let owned = hub.clone();
83 let CanonicalValue::Object(inner) = owned else {
84 unreachable!()
85 };
86 return Ok(inner
87 .into_iter()
88 .map(|(name, v)| (format!("{stem} ({name})"), v))
89 .collect());
90 }
91 }
92 Ok(vec![(stem, hub)])
93 }
94 }
95}
96
97fn decode_text(fmt: Format, text: &str) -> Result<CanonicalValue, Box<dyn Error>> {
98 match fmt {
99 Format::Csv => CanonicalValue::from_csv(text),
100 Format::Tsv => CanonicalValue::from_tsv(text),
101 Format::Json => CanonicalValue::from_json(text),
102 Format::Ndjson => CanonicalValue::from_ndjson(text),
103 Format::Yaml => CanonicalValue::from_yaml(text),
104 Format::Xml => CanonicalValue::from_xml(text),
105 Format::Toml => CanonicalValue::from_toml(text),
106 Format::Sql => {
107 let sheets = CanonicalValue::from_sql_workbook(text, SqlDialect::Generic)?;
108 if sheets.len() == 1 {
109 let mut it = sheets.into_iter();
110 Ok(it.next().unwrap().1)
111 } else {
112 Ok(CanonicalValue::Object(sheets))
113 }
114 }
115 Format::Xlsx | Format::Ods => Err(format!("{fmt:?} requires binary decode").into()),
116 }
117}
118
119pub fn render(
134 sources: Vec<(String, CanonicalValue)>,
135 to_json: bool,
136) -> Result<String, Box<dyn Error>> {
137 let mut out = String::new();
138 if to_json {
139 if sources.len() == 1 {
140 out.push_str(&sources[0].1.to_json()?);
141 out.push('\n');
142 } else {
143 let mut map = serde_json::Map::new();
144 for (label, value) in &sources {
145 map.insert(label.clone(), value.to_json_value());
146 }
147 out.push_str(&serde_json::to_string(&map)?);
148 out.push('\n');
149 }
150 return Ok(out);
151 }
152 for (i, (label, value)) in sources.iter().enumerate() {
153 if i > 0 {
154 out.push('\n');
155 }
156 out.push_str(&format!("Output table No. {} — {}\n", i + 1, label));
157 out.push_str(&render_table(value)?);
158 out.push('\n');
159 }
160 Ok(out)
161}
162
163fn render_table(value: &CanonicalValue) -> Result<String, Box<dyn Error>> {
164 let rows: Vec<CanonicalValue> = match value {
165 CanonicalValue::Array(a) => a.clone(),
166 CanonicalValue::Object(o) => vec![CanonicalValue::Object(o.clone())],
167 other => {
168 let mut t = Table::new();
169 t.load_preset(presets::UTF8_FULL);
170 t.add_row(vec![cell_from(other)]);
171 return Ok(t.to_string());
172 }
173 };
174
175 let mut headers: Vec<String> = Vec::new();
176 for row in &rows {
177 if let CanonicalValue::Object(o) = row {
178 for (k, _) in o {
179 if !headers.contains(k) {
180 headers.push(k.clone());
181 }
182 }
183 }
184 }
185
186 if headers.is_empty() {
187 return Ok(String::new());
188 }
189
190 let mut table = Table::new();
191 table.load_preset(presets::UTF8_FULL);
192 table.set_content_arrangement(ContentArrangement::Dynamic);
193 table.set_header(headers.iter().map(Cell::new));
194
195 for row in &rows {
196 if let CanonicalValue::Object(o) = row {
197 let cells: Vec<Cell> = headers
198 .iter()
199 .map(|h| match o.iter().find(|(k, _)| k == h).map(|(_, v)| v) {
200 Some(v) => cell_from(v),
201 None => Cell::new(""),
202 })
203 .collect();
204 table.add_row(cells);
205 }
206 }
207
208 Ok(table.to_string())
209}
210
211fn cell_from(v: &CanonicalValue) -> Cell {
212 match v {
213 CanonicalValue::Null => Cell::new(""),
214 CanonicalValue::Bool(b) => Cell::new(b.to_string()),
215 CanonicalValue::Number(n) => Cell::new(n.to_string()).set_alignment(CellAlignment::Right),
216 CanonicalValue::String(s) => Cell::new(s),
217 CanonicalValue::Bytes(b) => Cell::new(format!("<{} bytes>", b.len())),
218 CanonicalValue::Array(_) | CanonicalValue::Object(_) => Cell::new("<nested>"),
219 }
220}