From adbca874ab34f075caa71cee531b746e9098e52c Mon Sep 17 00:00:00 2001 From: drassi Date: Tue, 10 Mar 2026 17:26:06 -0400 Subject: [PATCH] fix: handle array-of-arrays in CSV formatter (#288) * fix: handle array-of-arrays in CSV formatter The CSV formatter assumed all items were JSON objects when collecting column names. APIs that return arrays of arrays (e.g. Sheets values) produced empty newlines instead of data. Add an early return path for non-object arrays that emits each inner array's elements as CSV cells directly, mirroring the table formatter's existing "array of non-objects" handling. Fixes #283 Co-Authored-By: Claude Opus 4.6 * test: add coverage for flat scalar CSV formatting --------- Co-authored-by: Dan Rassi <129646+drassi@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- .changeset/fix-csv-array-of-arrays.md | 5 +++ src/formatter.rs | 58 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 .changeset/fix-csv-array-of-arrays.md diff --git a/.changeset/fix-csv-array-of-arrays.md b/.changeset/fix-csv-array-of-arrays.md new file mode 100644 index 0000000..b0682ed --- /dev/null +++ b/.changeset/fix-csv-array-of-arrays.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Fix `--format csv` for array-of-arrays responses (e.g. Sheets values API) diff --git a/src/formatter.rs b/src/formatter.rs index 1b06966..08d4d28 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -356,6 +356,23 @@ fn format_csv_page(value: &Value, emit_header: bool) -> String { return String::new(); } + // Array of non-objects + if !arr.iter().any(|v| v.is_object()) { + let mut output = String::new(); + for item in arr { + if let Value::Array(inner) = item { + let cells: Vec = inner + .iter() + .map(|v| csv_escape(&value_to_cell(v))) + .collect(); + let _ = writeln!(output, "{}", cells.join(",")); + } else { + let _ = writeln!(output, "{}", csv_escape(&value_to_cell(item))); + } + } + return output; + } + // Collect columns let mut columns: Vec = Vec::new(); for item in arr { @@ -564,6 +581,47 @@ mod tests { assert!(output.contains("2,world")); } + #[test] + fn test_format_csv_array_of_arrays() { + // Sheets API returns {"values": [["col1","col2"], ["a","b"]]} + let val = json!({ + "values": [ + ["Student Name", "Gender", "Class Level"], + ["Alexandra", "Female", "4. Senior"], + ["Andrew", "Male", "1. Freshman"] + ] + }); + let output = format_value(&val, &OutputFormat::Csv); + let lines: Vec<&str> = output.lines().collect(); + assert_eq!(lines[0], "Student Name,Gender,Class Level"); + assert_eq!(lines[1], "Alexandra,Female,4. Senior"); + assert_eq!(lines[2], "Andrew,Male,1. Freshman"); + } + + #[test] + fn test_format_csv_flat_scalars() { + // Flat array of non-object, non-array values → one value per line + let val = json!(["apple", "banana", "cherry"]); + let output = format_value(&val, &OutputFormat::Csv); + let lines: Vec<&str> = output.lines().collect(); + assert_eq!(lines.len(), 3); + assert_eq!(lines[0], "apple"); + assert_eq!(lines[1], "banana"); + assert_eq!(lines[2], "cherry"); + } + + #[test] + fn test_format_csv_flat_scalars_with_escaping() { + // Scalars that contain commas/quotes must be CSV-escaped + let val = json!(["plain", "has,comma", "has\"quote"]); + let output = format_value(&val, &OutputFormat::Csv); + let lines: Vec<&str> = output.lines().collect(); + assert_eq!(lines.len(), 3); + assert_eq!(lines[0], "plain"); + assert_eq!(lines[1], "\"has,comma\""); + assert_eq!(lines[2], "\"has\"\"quote\""); + } + #[test] fn test_format_csv_escape() { assert_eq!(csv_escape("simple"), "simple");