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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>
This commit is contained in:
drassi
2026-03-10 17:26:06 -04:00
committed by GitHub
parent bd5041e3da
commit adbca874ab
2 changed files with 63 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Fix `--format csv` for array-of-arrays responses (e.g. Sheets values API)
+58
View File
@@ -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<String> = 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<String> = 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");