Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/schema-repeated-array-params.md
Original file line number Diff line number Diff line change
@@ -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
107 changes: 87 additions & 20 deletions src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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)?;
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -1269,7 +1297,46 @@ mod tests {

let (url, query) = build_url(&doc, &method, &params, 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, &params, 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]
Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
};

Expand Down
19 changes: 19 additions & 0 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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(&param);
assert_eq!(json["type"], "string");
assert_eq!(json["repeated"], true);
}

#[test]
Expand Down
Loading