fix(sheets): preserve multi-row structure in +append --json-values (#410)

* fix(sheets): preserve multi-row structure in +append --json-values

Previously, `parse_append_args` called `.flatten()` on the parsed
`Vec<Vec<String>>`, collapsing all rows into a single flat vector.
Combined with `build_append_request` wrapping the flat vec in another
array, this meant `[["Alice","100"],["Bob","200"]]` produced one row
with four columns instead of two rows with two columns each.

Change `AppendConfig.values` from `Vec<String>` to `Vec<Vec<String>>`
so row boundaries are preserved end-to-end. Single-row inputs via
`--values` and flat JSON arrays are wrapped in a single-element outer
vec for consistency.

Closes #311

* chore: trigger CLA re-check

* fix: warn on malformed --json-values input instead of silently ignoring

Address review feedback: print a warning to stderr when --json-values
cannot be parsed as a JSON array, rather than silently falling back
to an empty value set.
This commit is contained in:
Anshul Garg
2026-03-14 00:43:07 +05:30
committed by GitHub
parent 4b827cdd25
commit 4d7b420d53
2 changed files with 67 additions and 15 deletions
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Fix `+append --json-values` flattening multi-row arrays into a single row by preserving the `Vec<Vec<String>>` row structure through to the API request body
+62 -15
View File
@@ -218,11 +218,8 @@ fn build_append_request(
"valueInputOption": "USER_ENTERED"
});
// We use `json!` macro to construct a generic JSON Value for the request body.
// This allows us to easily create nested objects without defining explicit structs
// for every API request body.
let body = json!({
"values": [config.values]
"values": config.values
});
// Map `&String` scope URLs to owned `String`s for the return value
@@ -263,24 +260,27 @@ fn build_read_request(
pub struct AppendConfig {
/// The ID of the spreadsheet to append to.
pub spreadsheet_id: String,
/// The values to append, as a vector of strings.
pub values: Vec<String>,
/// The rows to append, where each inner Vec represents one row.
pub values: Vec<Vec<String>>,
}
/// Parses arguments for the `+append` command.
///
/// Splits the comma-separated `values` argument into a `Vec<String>`.
/// Supports both `--values` (single row) and `--json-values` (single or multi-row).
pub fn parse_append_args(matches: &ArgMatches) -> AppendConfig {
let values = if let Some(json_str) = matches.get_one::<String>("json-values") {
// Parse JSON array of rows
// Try parsing as array-of-arrays (multi-row) first
if let Ok(parsed) = serde_json::from_str::<Vec<Vec<String>>>(json_str) {
parsed.into_iter().flatten().collect()
parsed
} else if let Ok(parsed) = serde_json::from_str::<Vec<String>>(json_str) {
// Single flat array — treat as one row
vec![parsed]
} else {
// Treat as single row JSON array
serde_json::from_str::<Vec<String>>(json_str).unwrap_or_default()
eprintln!("Warning: --json-values is not valid JSON; expected an array or array-of-arrays");
Vec::new()
}
} else if let Some(values_str) = matches.get_one::<String>("values") {
values_str.split(',').map(|s| s.to_string()).collect()
vec![values_str.split(',').map(|s| s.to_string()).collect()]
} else {
Vec::new()
};
@@ -365,7 +365,7 @@ mod tests {
let doc = make_mock_doc();
let config = AppendConfig {
spreadsheet_id: "123".to_string(),
values: vec!["a".to_string(), "b".to_string(), "c".to_string()],
values: vec![vec!["a".to_string(), "b".to_string(), "c".to_string()]],
};
let (params, body, scopes) = build_append_request(&config, &doc).unwrap();
@@ -391,11 +391,58 @@ mod tests {
}
#[test]
fn test_parse_append_args() {
fn test_parse_append_args_values() {
let matches = make_matches_append(&["test", "--spreadsheet", "123", "--values", "a,b,c"]);
let config = parse_append_args(&matches);
assert_eq!(config.spreadsheet_id, "123");
assert_eq!(config.values, vec!["a", "b", "c"]);
assert_eq!(config.values, vec![vec!["a", "b", "c"]]);
}
#[test]
fn test_parse_append_args_json_single_row() {
let matches = make_matches_append(&[
"test",
"--spreadsheet",
"123",
"--json-values",
r#"["a","b","c"]"#,
]);
let config = parse_append_args(&matches);
assert_eq!(config.values, vec![vec!["a", "b", "c"]]);
}
#[test]
fn test_parse_append_args_json_multi_row() {
let matches = make_matches_append(&[
"test",
"--spreadsheet",
"123",
"--json-values",
r#"[["Alice","100"],["Bob","200"]]"#,
]);
let config = parse_append_args(&matches);
assert_eq!(
config.values,
vec![vec!["Alice", "100"], vec!["Bob", "200"]]
);
}
#[test]
fn test_build_append_request_multi_row() {
let doc = make_mock_doc();
let config = AppendConfig {
spreadsheet_id: "123".to_string(),
values: vec![
vec!["Alice".to_string(), "100".to_string()],
vec!["Bob".to_string(), "200".to_string()],
],
};
let (_params, body, _scopes) = build_append_request(&config, &doc).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
let values = parsed["values"].as_array().unwrap();
assert_eq!(values.len(), 2);
assert_eq!(values[0], json!(["Alice", "100"]));
assert_eq!(values[1], json!(["Bob", "200"]));
}
#[test]