From 24d82d50d8fa8d5e740c2e24dfa511d33294f9eb Mon Sep 17 00:00:00 2001 From: Pablo Garcia Pizano Date: Mon, 9 Mar 2026 00:09:03 -0500 Subject: [PATCH 1/2] [night-shift] fix: fix +append --json-values multi-row bug Change AppendConfig.values from Vec to Vec> so that multi-row JSON input is preserved as a 2D array instead of being flattened. Update parse_append_args to keep the parsed Vec> directly, wrap --values comma path in an outer vec, and fix build_append_request to pass config.values directly. Add new test_parse_append_args_json_multirow test. Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-append-json-multirow.md | 5 +++ src/helpers/sheets.rs | 43 ++++++++++++++++++++------ 2 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 .changeset/fix-append-json-multirow.md diff --git a/.changeset/fix-append-json-multirow.md b/.changeset/fix-append-json-multirow.md new file mode 100644 index 00000000..1e3f6de0 --- /dev/null +++ b/.changeset/fix-append-json-multirow.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Fix `+append --json-values` multi-row bug by changing AppendConfig.values to Vec> diff --git a/src/helpers/sheets.rs b/src/helpers/sheets.rs index 5c2a9e90..a65302fb 100644 --- a/src/helpers/sheets.rs +++ b/src/helpers/sheets.rs @@ -222,7 +222,7 @@ fn build_append_request( // 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,8 +263,8 @@ 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, + /// The values to append, as a 2D array of strings (one inner vec per row). + pub values: Vec>, } /// Parses arguments for the `+append` command. @@ -274,13 +274,15 @@ pub fn parse_append_args(matches: &ArgMatches) -> AppendConfig { let values = if let Some(json_str) = matches.get_one::("json-values") { // Parse JSON array of rows if let Ok(parsed) = serde_json::from_str::>>(json_str) { - parsed.into_iter().flatten().collect() + parsed } else { // Treat as single row JSON array - serde_json::from_str::>(json_str).unwrap_or_default() + serde_json::from_str::>(json_str) + .map(|row| vec![row]) + .unwrap_or_default() } } else if let Some(values_str) = matches.get_one::("values") { - values_str.split(',').map(|s| s.to_string()).collect() + vec![values_str.split(',').map(|s| s.to_string()).collect()] } else { Vec::new() }; @@ -365,14 +367,18 @@ 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(); assert!(params.contains("123")); assert!(params.contains("USER_ENTERED")); - assert!(body.contains("a")); - assert!(body.contains("b")); + + let body_json: serde_json::Value = serde_json::from_str(&body).unwrap(); + assert_eq!( + body_json, + serde_json::json!({"values": [["a", "b", "c"]]}) + ); assert_eq!(scopes[0], "https://scope"); } @@ -395,7 +401,24 @@ mod tests { 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_multirow() { + let matches = make_matches_append(&[ + "test", + "--spreadsheet", + "123", + "--json-values", + r#"[["Alice","100"],["Bob","200"]]"#, + ]); + let config = parse_append_args(&matches); + assert_eq!(config.spreadsheet_id, "123"); + assert_eq!( + config.values, + vec![vec!["Alice", "100"], vec!["Bob", "200"]] + ); } #[test] From 7a48eab93676137f07a6c0ed8ee21eb6ee40338c Mon Sep 17 00:00:00 2001 From: Pablo Garcia Pizano Date: Wed, 11 Mar 2026 19:54:35 -0500 Subject: [PATCH 2/2] Return validation error for malformed --json-values input Address PR review feedback: parse_append_args now returns Result instead of silently falling back to an empty list via unwrap_or_default(). Malformed JSON input surfaces a clear GwsError::Validation error message. Co-Authored-By: Claude Opus 4.6 --- src/helpers/sheets.rs | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/helpers/sheets.rs b/src/helpers/sheets.rs index a65302fb..b179c75c 100644 --- a/src/helpers/sheets.rs +++ b/src/helpers/sheets.rs @@ -102,7 +102,7 @@ TIPS: ) -> Pin> + Send + 'a>> { Box::pin(async move { if let Some(matches) = matches.subcommand_matches("+append") { - let config = parse_append_args(matches); + let config = parse_append_args(matches)?; let (params_str, body_str, scopes) = build_append_request(&config, doc)?; let scope_strs: Vec<&str> = scopes.iter().map(|s| s.as_str()).collect(); @@ -260,6 +260,7 @@ fn build_read_request( /// Configuration for appending values to a spreadsheet. /// /// Holds the parsed arguments for the `+append` subcommand. +#[derive(Debug)] pub struct AppendConfig { /// The ID of the spreadsheet to append to. pub spreadsheet_id: String, @@ -270,16 +271,19 @@ pub struct AppendConfig { /// Parses arguments for the `+append` command. /// /// Splits the comma-separated `values` argument into a `Vec`. -pub fn parse_append_args(matches: &ArgMatches) -> AppendConfig { +pub fn parse_append_args(matches: &ArgMatches) -> Result { let values = if let Some(json_str) = matches.get_one::("json-values") { - // Parse JSON array of rows + // Try parsing as 2D array first, then fall back to 1D array if let Ok(parsed) = serde_json::from_str::>>(json_str) { parsed } else { - // Treat as single row JSON array serde_json::from_str::>(json_str) .map(|row| vec![row]) - .unwrap_or_default() + .map_err(|e| { + GwsError::Validation(format!( + "Invalid --json-values: {e}. Expected a JSON array of strings or array of arrays." + )) + })? } } else if let Some(values_str) = matches.get_one::("values") { vec![values_str.split(',').map(|s| s.to_string()).collect()] @@ -287,10 +291,10 @@ pub fn parse_append_args(matches: &ArgMatches) -> AppendConfig { Vec::new() }; - AppendConfig { + Ok(AppendConfig { spreadsheet_id: matches.get_one::("spreadsheet").unwrap().clone(), values, - } + }) } /// Configuration for reading values from a spreadsheet. @@ -399,7 +403,7 @@ mod tests { #[test] fn test_parse_append_args() { let matches = make_matches_append(&["test", "--spreadsheet", "123", "--values", "a,b,c"]); - let config = parse_append_args(&matches); + let config = parse_append_args(&matches).unwrap(); assert_eq!(config.spreadsheet_id, "123"); assert_eq!(config.values, vec![vec!["a", "b", "c"]]); } @@ -413,7 +417,7 @@ mod tests { "--json-values", r#"[["Alice","100"],["Bob","200"]]"#, ]); - let config = parse_append_args(&matches); + let config = parse_append_args(&matches).unwrap(); assert_eq!(config.spreadsheet_id, "123"); assert_eq!( config.values, @@ -421,6 +425,21 @@ mod tests { ); } + #[test] + fn test_parse_append_args_json_malformed() { + let matches = make_matches_append(&[ + "test", + "--spreadsheet", + "123", + "--json-values", + "not valid json", + ]); + let result = parse_append_args(&matches); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Invalid --json-values")); + } + #[test] fn test_parse_read_args() { let matches = make_matches_read(&["test", "--spreadsheet", "123", "--range", "A1:B2"]);