Skip to main content

excel_cli/cli/
output.rs

1use serde_json::Value;
2
3use crate::cli::args::OutputFormat;
4use crate::cli::common::tab_separated_values;
5use crate::cli::error::AppError;
6
7/// Write a success value to stdout.
8pub fn write_success(value: &Value, format: &OutputFormat) -> Result<(), AppError> {
9    if value["meta"]["output_shape"].as_str() == Some("jsonl") {
10        return write_jsonl_records(value);
11    }
12
13    match format {
14        OutputFormat::Json => {
15            let s = serde_json::to_string_pretty(value).map_err(|e| AppError::InternalError {
16                message: format!("JSON serialization failed: {}", e),
17            })?;
18            println!("{}", s);
19        }
20        OutputFormat::Text => {
21            write_text(value)?;
22        }
23    }
24    Ok(())
25}
26
27fn write_jsonl_records(value: &Value) -> Result<(), AppError> {
28    let Some(records) = value["data"]["records"].as_array() else {
29        return Err(AppError::InternalError {
30            message: "JSONL output requires record data".to_string(),
31        });
32    };
33
34    for record in records {
35        let s = serde_json::to_string(record).map_err(|e| AppError::InternalError {
36            message: format!("JSON serialization failed: {}", e),
37        })?;
38        println!("{}", s);
39    }
40
41    Ok(())
42}
43
44/// Write an error value to stderr.
45pub fn write_error(value: &Value) {
46    if let Ok(s) = serde_json::to_string_pretty(value) {
47        eprintln!("{}", s);
48    } else {
49        eprintln!("{{\"error\":\"Internal JSON serialization error\"}}");
50    }
51}
52
53/// Best-effort text rendering for successful envelopes.
54fn write_text(value: &Value) -> Result<(), AppError> {
55    let command = value["command"].as_str().unwrap_or("unknown");
56
57    match command {
58        "inspect.workbook" => write_text_workbook(value)?,
59        "inspect.sheet" => write_text_sheet(value)?,
60        "inspect.sample" => write_text_sample(value)?,
61        "inspect.columns" => write_text_columns(value)?,
62        "inspect.tables" => write_text_tables(value)?,
63        "read.cell" => write_text_cell(value)?,
64        "read.range" => write_text_range(value)?,
65        "read.rows" => write_text_rows(value)?,
66        _ => {
67            // Fallback to JSON for unknown commands
68            let s = serde_json::to_string_pretty(value).map_err(|e| AppError::InternalError {
69                message: format!("JSON serialization failed: {}", e),
70            })?;
71            println!("{}", s);
72        }
73    }
74
75    Ok(())
76}
77
78fn write_text_workbook(value: &Value) -> Result<(), AppError> {
79    let data = &value["data"];
80    if let Some(sheets) = data["sheets"].as_array() {
81        for sheet in sheets {
82            let name = sheet["name"].as_str().unwrap_or("");
83            let index = sheet["index"].as_u64().unwrap_or(0);
84            println!("{}	{}", index, name);
85        }
86    }
87    Ok(())
88}
89
90fn write_text_sheet(value: &Value) -> Result<(), AppError> {
91    let data = &value["data"];
92    if let Some(name) = data["name"].as_str() {
93        println!("name\t{}", name);
94    }
95    if let Some(index) = data["index"].as_u64() {
96        println!("index\t{}", index);
97    }
98    if let Some(max_rows) = data["max_rows"].as_u64() {
99        println!("max_rows\t{}", max_rows);
100    }
101    if let Some(max_cols) = data["max_cols"].as_u64() {
102        println!("max_cols\t{}", max_cols);
103    }
104    if let Some(used_range) = data["used_range"].as_str() {
105        println!("used_range\t{}", used_range);
106    }
107    Ok(())
108}
109
110fn write_text_sample(value: &Value) -> Result<(), AppError> {
111    let data = &value["data"];
112    if let Some(rows) = data["rows"].as_array() {
113        write_row_arrays(rows);
114    } else if let Some(records) = data["records"].as_array() {
115        write_record_objects(records);
116    }
117    Ok(())
118}
119
120fn write_text_columns(value: &Value) -> Result<(), AppError> {
121    println!("index\tname\tsafe_name\tis_duplicate\tinferred_type\tnon_null_ratio\tformula_ratio");
122
123    if let Some(columns) = value["data"]["columns"].as_array() {
124        for column in columns {
125            let index = column["index"].as_u64().unwrap_or(0);
126            let name = column["name"].as_str().unwrap_or("");
127            let safe_name = column["safe_name"].as_str().unwrap_or("");
128            let is_duplicate = column["is_duplicate"].as_bool().unwrap_or(false);
129            let inferred_type = column["inferred_type"].as_str().unwrap_or("");
130            let non_null_ratio = format_ratio(column["non_null_ratio"].as_f64().unwrap_or(0.0));
131            let formula_ratio = format_ratio(column["formula_ratio"].as_f64().unwrap_or(0.0));
132
133            println!(
134                "{}\t{}\t{}\t{}\t{}\t{}\t{}",
135                index, name, safe_name, is_duplicate, inferred_type, non_null_ratio, formula_ratio
136            );
137        }
138    }
139
140    Ok(())
141}
142
143fn format_ratio(value: f64) -> String {
144    let rounded = (value * 1000.0).round() / 1000.0;
145    if (rounded.fract()).abs() < f64::EPSILON {
146        format!("{rounded:.0}")
147    } else {
148        let formatted = format!("{rounded:.3}");
149        formatted
150            .trim_end_matches('0')
151            .trim_end_matches('.')
152            .to_string()
153    }
154}
155
156fn write_text_tables(value: &Value) -> Result<(), AppError> {
157    let data = &value["data"];
158    if let Some(candidates) = data["candidates"].as_array() {
159        for candidate in candidates {
160            let range = candidate["range"].as_str().unwrap_or("");
161            let header_row = candidate["header_row"].as_u64().unwrap_or(0);
162            let column_count = candidate["column_count"].as_u64().unwrap_or(0);
163            let row_count = candidate["row_count"].as_u64().unwrap_or(0);
164            let confidence = candidate["confidence"].as_f64().unwrap_or(0.0);
165            println!(
166                "{}\theader_row={}\tcolumns={}\trows={}\tconfidence={:.2}",
167                range, header_row, column_count, row_count, confidence
168            );
169        }
170    }
171    Ok(())
172}
173
174fn write_text_cell(value: &Value) -> Result<(), AppError> {
175    if let Some(v) = value["data"]["value"].as_str() {
176        println!("{}", v);
177    } else {
178        println!("{}", value["data"]["value"]);
179    }
180    Ok(())
181}
182
183fn write_text_range(value: &Value) -> Result<(), AppError> {
184    let data = &value["data"];
185    if let Some(rows) = data["rows"].as_array() {
186        write_row_arrays(rows);
187    }
188    Ok(())
189}
190
191fn write_text_rows(value: &Value) -> Result<(), AppError> {
192    let data = &value["data"];
193    if let Some(rows) = data["rows"].as_array() {
194        write_row_arrays(rows);
195    } else if let Some(records) = data["records"].as_array() {
196        write_record_objects(records);
197    }
198    Ok(())
199}
200
201fn write_row_arrays(rows: &[Value]) {
202    for row in rows {
203        if let Some(cells) = row.as_array() {
204            println!("{}", tab_separated_values(cells));
205        }
206    }
207}
208
209fn write_record_objects(records: &[Value]) {
210    for record in records {
211        if let Some(obj) = record.as_object() {
212            let parts: Vec<String> = obj.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
213            println!("{}", parts.join("\t"));
214        }
215    }
216}