Skip to content
Closed
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
8 changes: 8 additions & 0 deletions .changeset/allow-unknown-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@googleworkspace/cli": minor
---

Add `--allow-unknown-fields` to raw API methods with JSON request bodies. Explicitly
allow fields absent from Discovery recursively, including in dry runs, while
preserving validation of known fields, required fields, JSON, URLs and file paths.
Handwritten helpers retain strict validation.
5 changes: 5 additions & 0 deletions .changeset/current-clippy-baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---

Keep Apps Script file selection compatible with the current Clippy checks.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,44 @@ gws schema drive.files.list
gws drive files list --params '{"pageSize": 100}' --page-all | jq -r '.files[].name'
```

### Fields absent from Discovery

Raw API methods with a request body accept `--allow-unknown-fields` alongside
`--json`. Use it explicitly when an API supports fields that its public Discovery
document does not yet describe. It allows unknown properties recursively,
including nested objects and array elements, and forwards their values unchanged.
JSON is still parsed and serialized normally; whitespace and key order may change.

Validation remains strict by default. With the flag, known-field types, enums and
required fields are still checked, as are JSON syntax, required URL parameters and
file paths. It does not allow new enum values on a known field. The flag is local
to raw methods and does not apply to handwritten `+` helpers.

For example, Docs suggestions and comments require a Cloud project enrolled in the
[Google Workspace Developer Preview Program](https://developers.google.com/workspace/preview).
Google still enforces API availability, OAuth scopes, document permissions and
server-side validation. This flag grants no additional access.

```bash
# Preview a suggested insertion (Docs Developer Preview).
gws docs documents batchUpdate \
--params '{"documentId":"DOCUMENT_ID"}' \
--json '{"requests":[{"insertText":{"location":{"index":1},"text":"Suggested text"}}],"writeControl":{"writeMode":"SUGGEST"}}' \
--allow-unknown-fields --dry-run

# Preview a comment anchored to existing text; adjust the range for your document.
gws docs documents batchUpdate \
--params '{"documentId":"DOCUMENT_ID"}' \
--json '{"requests":[{"insertComment":{"content":"Please review this text.","range":{"startIndex":1,"endIndex":5}}}]}' \
--allow-unknown-fields --dry-run
```

`--dry-run` uses the same validation policy and shows the request without sending
it. It cannot verify preview enrollment or server acceptance. Remove `--dry-run`
to submit a request. See the Docs
[request reference](https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/request#InsertCommentRequest)
for preview field requirements.

## Authentication

The CLI supports multiple auth workflows so it works on your laptop, in CI, and on a server.
Expand Down
114 changes: 108 additions & 6 deletions crates/google-workspace-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,20 @@ fn build_resource_command(name: &str, resource: &RestResource) -> Option<Command

// Only add --json flag if the method accepts a request body
if method.request.is_some() {
method_cmd = method_cmd.arg(
Arg::new("json")
.long("json")
.help("JSON string for the request body")
.value_name("JSON"),
);
method_cmd = method_cmd
.arg(
Arg::new("json")
.long("json")
.help("JSON string for the request body")
.value_name("JSON"),
)
.arg(
Arg::new("allow-unknown-fields")
.long("allow-unknown-fields")
.help("Allow request fields absent from Discovery, including nested fields; known fields are still validated")
.action(clap::ArgAction::SetTrue)
.requires("json"),
);
}

// Add --upload flag if the method supports media upload
Expand Down Expand Up @@ -251,6 +259,100 @@ mod tests {
}
}

#[test]
fn test_allow_unknown_fields_on_body_methods_and_subresources() {
let mut doc = make_doc();
let resource = || RestResource {
methods: HashMap::from([(
"create".to_string(),
RestMethod {
request: Some(crate::discovery::SchemaRef {
schema_ref: Some("File".to_string()),
parameter_name: None,
}),
..Default::default()
},
)]),
..Default::default()
};
doc.resources
.get_mut("files")
.unwrap()
.resources
.insert("comments".to_string(), resource());
doc.resources.insert("uploads".to_string(), resource());

for path in [
vec!["gws", "uploads", "create"],
vec!["gws", "files", "comments", "create"],
] {
let mut args = path;
args.extend(["--json", r#"{"preview": true}"#, "--allow-unknown-fields"]);
let result = build_cli(&doc).try_get_matches_from(args);
assert!(
result.is_ok(),
"body methods must accept opt-in: {result:?}"
);
}
}

#[test]
fn test_allow_unknown_fields_rejects_bodyless_methods_and_global_use() {
let doc = make_doc();
for args in [
vec!["gws", "files", "list", "--allow-unknown-fields"],
vec!["gws", "files", "delete", "--allow-unknown-fields"],
vec!["gws", "--allow-unknown-fields", "files", "list"],
vec!["gws", "files", "--allow-unknown-fields", "list"],
] {
let err = build_cli(&doc).try_get_matches_from(args).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
}
}

#[test]
fn test_allow_unknown_fields_rejects_helpers() {
let doc = RestDescription {
name: "docs".to_string(),
..Default::default()
};
let err = build_cli(&doc)
.try_get_matches_from([
"gws",
"+write",
"--document",
"test-document",
"--text",
"Test",
"--allow-unknown-fields",
])
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
}

#[test]
fn test_allow_unknown_fields_requires_json() {
let mut doc = make_doc();
doc.resources
.get_mut("files")
.unwrap()
.methods
.get_mut("list")
.unwrap()
.request = Some(crate::discovery::SchemaRef {
schema_ref: Some("File".to_string()),
parameter_name: None,
});
// An optional body stays optional when the opt-in is absent.
assert!(build_cli(&doc)
.try_get_matches_from(["gws", "files", "list"])
.is_ok());
let err = build_cli(&doc)
.try_get_matches_from(["gws", "files", "list", "--allow-unknown-fields"])
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
}

#[test]
fn test_all_commands_always_shown() {
let doc = make_doc();
Expand Down
Loading
Loading