fix(schema): expose repeated field and expand array query params (#415)
* fix(schema): expose repeated field and expand array query params Two related fixes for repeated/array query parameters: 1. `gws schema` now includes `"repeated": true` in parameter output when the Discovery Document marks a parameter as repeated. This lets users know which params accept multiple values. 2. When `--params` contains a JSON array for a parameter marked `repeated: true`, the executor now expands it into multiple query parameters (e.g. `?h=Subject&h=Date&h=From`) instead of stringifying the array as a single value. The query_params type changes from HashMap<String, String> to Vec<(String, String)> to support multiple entries with the same key. Closes #300 * fix: warn when array is passed for a non-repeated query parameter Address review feedback: print a warning to stderr when a JSON array is provided for a parameter not marked as repeated, since the array will be stringified rather than expanded. Directs users to gws schema to check which parameters accept arrays. * fix: import MethodParameter in executor test module The test `test_build_url_repeated_query_param_expands_array` uses MethodParameter but it was not imported in the test module's use statement, causing a compilation error in CI. * fix: pass all query params in a single request.query() call Consolidate query parameters (including pageToken) into a single request.query() call to ensure repeated parameters are sent correctly instead of being overwritten by successive calls.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@googleworkspace/cli": patch
|
||||
---
|
||||
|
||||
Expose `repeated: true` in `gws schema` output and expand JSON arrays into repeated query parameters for `repeated` fields
|
||||
+87
-20
@@ -65,7 +65,7 @@ struct ExecutionInput {
|
||||
params: Map<String, Value>,
|
||||
body: Option<Value>,
|
||||
full_url: String,
|
||||
query_params: HashMap<String, String>,
|
||||
query_params: Vec<(String, String)>,
|
||||
is_upload: bool,
|
||||
}
|
||||
|
||||
@@ -170,12 +170,12 @@ async fn build_http_request(
|
||||
request = request.header("x-goog-user-project", quota_project);
|
||||
}
|
||||
|
||||
for (key, value) in &input.query_params {
|
||||
request = request.query(&[(key, value)]);
|
||||
}
|
||||
|
||||
let mut all_query_params = input.query_params.clone();
|
||||
if let Some(pt) = page_token {
|
||||
request = request.query(&[("pageToken", pt)]);
|
||||
all_query_params.push(("pageToken".to_string(), pt.to_string()));
|
||||
}
|
||||
if !all_query_params.is_empty() {
|
||||
request = request.query(&all_query_params);
|
||||
}
|
||||
|
||||
if pages_fetched == 0 {
|
||||
@@ -484,7 +484,7 @@ fn build_url(
|
||||
method: &RestMethod,
|
||||
params: &Map<String, Value>,
|
||||
is_upload: bool,
|
||||
) -> Result<(String, HashMap<String, String>), GwsError> {
|
||||
) -> Result<(String, Vec<(String, String)>), GwsError> {
|
||||
// Build URL base and path
|
||||
|
||||
// Actually we need to construct base URL properly if not present
|
||||
@@ -521,14 +521,9 @@ fn build_url(
|
||||
|
||||
// Substitute path parameters and separate query parameters
|
||||
let path_parameters = extract_template_path_parameters(path_template);
|
||||
let mut query_params: HashMap<String, String> = HashMap::new();
|
||||
let mut query_params: Vec<(String, String)> = Vec::new();
|
||||
|
||||
for (key, value) in params {
|
||||
let val_str = match value {
|
||||
Value::String(s) => s.clone(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
|
||||
if path_parameters.contains(key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
@@ -546,8 +541,39 @@ fn build_url(
|
||||
)));
|
||||
}
|
||||
|
||||
// It's a query parameter
|
||||
query_params.insert(key.clone(), val_str);
|
||||
// For repeated parameters, expand JSON arrays into multiple query entries
|
||||
let is_repeated = method
|
||||
.parameters
|
||||
.get(key)
|
||||
.map(|p| p.repeated)
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_repeated {
|
||||
if let Value::Array(arr) = value {
|
||||
for item in arr {
|
||||
let val_str = match item {
|
||||
Value::String(s) => s.clone(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
query_params.push((key.clone(), val_str));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if !is_repeated && value.is_array() {
|
||||
eprintln!(
|
||||
"Warning: parameter '{}' is not marked as repeated; array value will be stringified. \
|
||||
Use `gws schema` to check which parameters accept arrays.",
|
||||
key
|
||||
);
|
||||
}
|
||||
|
||||
let val_str = match value {
|
||||
Value::String(s) => s.clone(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
query_params.push((key.clone(), val_str));
|
||||
}
|
||||
|
||||
let url_path = render_path_template(path_template, params)?;
|
||||
@@ -966,7 +992,9 @@ pub fn mime_to_extension(mime: &str) -> &str {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::discovery::{JsonSchema, JsonSchemaProperty, RestDescription, RestMethod};
|
||||
use crate::discovery::{
|
||||
JsonSchema, JsonSchemaProperty, MethodParameter, RestDescription, RestMethod,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -1269,7 +1297,46 @@ mod tests {
|
||||
|
||||
let (url, query) = build_url(&doc, &method, ¶ms, false).unwrap();
|
||||
assert_eq!(url, "https://api.example.com/files");
|
||||
assert_eq!(query.get("q").unwrap(), "search term");
|
||||
assert_eq!(query, vec![("q".to_string(), "search term".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_repeated_query_param_expands_array() {
|
||||
let doc = RestDescription {
|
||||
base_url: Some("https://api.example.com/".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut method_params = HashMap::new();
|
||||
method_params.insert(
|
||||
"metadataHeaders".to_string(),
|
||||
MethodParameter {
|
||||
param_type: Some("string".to_string()),
|
||||
location: Some("query".to_string()),
|
||||
repeated: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let method = RestMethod {
|
||||
path: "messages".to_string(),
|
||||
flat_path: Some("messages".to_string()),
|
||||
parameters: method_params,
|
||||
..Default::default()
|
||||
};
|
||||
let mut params = Map::new();
|
||||
params.insert(
|
||||
"metadataHeaders".to_string(),
|
||||
json!(["Subject", "Date", "From"]),
|
||||
);
|
||||
|
||||
let (_url, query) = build_url(&doc, &method, ¶ms, false).unwrap();
|
||||
assert_eq!(
|
||||
query,
|
||||
vec![
|
||||
("metadataHeaders".to_string(), "Subject".to_string()),
|
||||
("metadataHeaders".to_string(), "Date".to_string()),
|
||||
("metadataHeaders".to_string(), "From".to_string()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1875,7 +1942,7 @@ async fn test_post_without_body_sets_content_length_zero() {
|
||||
full_url: "https://example.com/messages/trash".to_string(),
|
||||
body: None,
|
||||
params: Map::new(),
|
||||
query_params: HashMap::new(),
|
||||
query_params: Vec::new(),
|
||||
is_upload: false,
|
||||
};
|
||||
|
||||
@@ -1915,7 +1982,7 @@ async fn test_post_with_body_does_not_add_content_length_zero() {
|
||||
full_url: "https://example.com/files".to_string(),
|
||||
body: Some(json!({"name": "test"})),
|
||||
params: Map::new(),
|
||||
query_params: HashMap::new(),
|
||||
query_params: Vec::new(),
|
||||
is_upload: false,
|
||||
};
|
||||
|
||||
@@ -1953,7 +2020,7 @@ async fn test_get_does_not_set_content_length_zero() {
|
||||
full_url: "https://example.com/files".to_string(),
|
||||
body: None,
|
||||
params: Map::new(),
|
||||
query_params: HashMap::new(),
|
||||
query_params: Vec::new(),
|
||||
is_upload: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -210,6 +210,9 @@ fn param_to_json(param: &MethodParameter) -> Value {
|
||||
if let Some(ref vals) = param.enum_values {
|
||||
p["enum"] = json!(vals);
|
||||
}
|
||||
if param.repeated {
|
||||
p["repeated"] = json!(true);
|
||||
}
|
||||
if param.deprecated {
|
||||
p["deprecated"] = json!(true);
|
||||
}
|
||||
@@ -345,6 +348,22 @@ mod tests {
|
||||
assert_eq!(json["default"], "0");
|
||||
assert!(json["enum"].is_array());
|
||||
assert_eq!(json["deprecated"], true);
|
||||
// repeated: false should NOT appear in output
|
||||
assert!(json.get("repeated").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_param_to_json_repeated() {
|
||||
let param = MethodParameter {
|
||||
param_type: Some("string".to_string()),
|
||||
location: Some("query".to_string()),
|
||||
repeated: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let json = param_to_json(¶m);
|
||||
assert_eq!(json["type"], "string");
|
||||
assert_eq!(json["repeated"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user