-
Notifications
You must be signed in to change notification settings - Fork 529
feat: add GOOGLE_WORKSPACE_CLI_API_BASE_URL for custom/mock endpoint support #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
30e0f85
f3c07af
76c370e
7cc5875
d9c97ce
c00f0d8
88d026f
e2f436a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@googleworkspace/cli": minor | ||
| --- | ||
|
|
||
| Add `GOOGLE_WORKSPACE_CLI_API_BASE_URL` env var to redirect API requests to a custom endpoint (e.g., mock server). Authentication is automatically skipped when set. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -183,6 +183,21 @@ pub struct JsonSchemaProperty { | |
| pub additional_properties: Option<Box<JsonSchemaProperty>>, | ||
| } | ||
|
|
||
| static CUSTOM_API_BASE_URL: std::sync::LazyLock<Option<String>> = std::sync::LazyLock::new(|| { | ||
| let url = std::env::var("GOOGLE_WORKSPACE_CLI_API_BASE_URL") | ||
| .ok() | ||
| .filter(|s| !s.is_empty()); | ||
| if let Some(ref u) = url { | ||
| eprintln!("[gws] Custom API endpoint active: {u}"); | ||
| eprintln!("[gws] Authentication is disabled. Requests will NOT go to Google APIs."); | ||
| } | ||
| url | ||
| }); | ||
|
|
||
| pub fn custom_api_base_url() -> Option<&'static str> { | ||
| CUSTOM_API_BASE_URL.as_deref() | ||
| } | ||
xdotli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// Fetches and caches a Google Discovery Document. | ||
| pub async fn fetch_discovery_document( | ||
| service: &str, | ||
|
|
@@ -206,7 +221,8 @@ pub async fn fetch_discovery_document( | |
| if let Ok(modified) = metadata.modified() { | ||
| if modified.elapsed().unwrap_or_default() < std::time::Duration::from_secs(86400) { | ||
| let data = std::fs::read_to_string(&cache_file)?; | ||
| let doc: RestDescription = serde_json::from_str(&data)?; | ||
| let mut doc: RestDescription = serde_json::from_str(&data)?; | ||
| apply_base_url_override(&mut doc); | ||
| return Ok(doc); | ||
| } | ||
| } | ||
|
|
@@ -247,10 +263,33 @@ pub async fn fetch_discovery_document( | |
| let _ = e; | ||
| } | ||
|
|
||
| let doc: RestDescription = serde_json::from_str(&body)?; | ||
| let mut doc: RestDescription = serde_json::from_str(&body)?; | ||
| apply_base_url_override(&mut doc); | ||
| Ok(doc) | ||
| } | ||
|
|
||
| fn apply_base_url_override(doc: &mut RestDescription) { | ||
| if let Some(base) = custom_api_base_url() { | ||
| rewrite_base_url(doc, base); | ||
| } | ||
| } | ||
|
|
||
| fn rewrite_base_url(doc: &mut RestDescription, base: &str) { | ||
| let base_trimmed = base.trim_end_matches('/'); | ||
| let new_root_url = format!("{base_trimmed}/"); | ||
| let original_root_url = std::mem::replace(&mut doc.root_url, new_root_url); | ||
|
|
||
| if let Some(base_url) = &mut doc.base_url { | ||
| if let Some(stripped_path) = base_url.strip_prefix(&original_root_url) { | ||
| *base_url = format!("{}{}", &doc.root_url, stripped_path); | ||
| } else { | ||
| // Fallback: base_url has a different domain than root_url. | ||
| // Still rewrite to ensure requests go to the custom endpoint. | ||
| *base_url = doc.root_url.clone(); | ||
| } | ||
xdotli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
xdotli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
Comment on lines
+277
to
+291
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation of For example, a request to The fix is to replace only the host part of the fn rewrite_base_url(doc: &mut RestDescription, base: &str) {
let base_trimmed = base.trim_end_matches('/');
let new_root_url = format!("{base_trimmed}/");
let original_root_url = std::mem::replace(&mut doc.root_url, new_root_url);
if let Some(base_url) = &mut doc.base_url {
*base_url = base_url.replace(&original_root_url, &doc.root_url);
}
} |
||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
@@ -320,4 +359,74 @@ mod tests { | |
| assert!(doc.resources.is_empty()); | ||
| assert!(doc.schemas.is_empty()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_rewrite_base_url_empty_service_path() { | ||
| let mut doc = RestDescription { | ||
| name: "gmail".to_string(), | ||
| version: "v1".to_string(), | ||
| root_url: "https://gmail.googleapis.com/".to_string(), | ||
| base_url: Some("https://gmail.googleapis.com/".to_string()), | ||
| service_path: "".to_string(), | ||
| ..Default::default() | ||
| }; | ||
|
|
||
| rewrite_base_url(&mut doc, "http://localhost:8099"); | ||
| assert_eq!(doc.root_url, "http://localhost:8099/"); | ||
| assert_eq!(doc.base_url.as_deref(), Some("http://localhost:8099/")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_rewrite_base_url_preserves_service_path() { | ||
| let mut doc = RestDescription { | ||
| name: "drive".to_string(), | ||
| version: "v3".to_string(), | ||
| root_url: "https://www.googleapis.com/".to_string(), | ||
| base_url: Some("https://www.googleapis.com/drive/v3/".to_string()), | ||
| service_path: "drive/v3/".to_string(), | ||
| ..Default::default() | ||
| }; | ||
|
|
||
| rewrite_base_url(&mut doc, "http://localhost:8099/"); | ||
| assert_eq!(doc.root_url, "http://localhost:8099/"); | ||
| assert_eq!( | ||
| doc.base_url.as_deref(), | ||
| Some("http://localhost:8099/drive/v3/") | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_rewrite_base_url_different_domain_fallback() { | ||
| // Edge case: base_url has a different domain than root_url. | ||
| // The fallback should still rewrite base_url to the custom endpoint. | ||
| let mut doc = RestDescription { | ||
| name: "hypothetical".to_string(), | ||
| version: "v1".to_string(), | ||
| root_url: "https://hypothetical.googleapis.com/".to_string(), | ||
| base_url: Some("https://other-domain.googleapis.com/hypothetical/v1/".to_string()), | ||
| service_path: "hypothetical/v1/".to_string(), | ||
| ..Default::default() | ||
| }; | ||
|
|
||
| rewrite_base_url(&mut doc, "http://localhost:8099"); | ||
| assert_eq!(doc.root_url, "http://localhost:8099/"); | ||
| // Service path is lost but requests still go to the custom endpoint | ||
| assert_eq!(doc.base_url.as_deref(), Some("http://localhost:8099/")); | ||
| } | ||
xdotli marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| #[test] | ||
| fn test_rewrite_base_url_none() { | ||
| let mut doc = RestDescription { | ||
| name: "customsearch".to_string(), | ||
| version: "v1".to_string(), | ||
| root_url: "https://www.googleapis.com/".to_string(), | ||
| base_url: None, | ||
| service_path: "customsearch/v1/".to_string(), | ||
| ..Default::default() | ||
| }; | ||
|
|
||
| rewrite_base_url(&mut doc, "http://localhost:8099"); | ||
| assert_eq!(doc.root_url, "http://localhost:8099/"); | ||
| assert!(doc.base_url.is_none()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
eprintln!macro can panic if it fails to write to stderr, for instance, due to a broken pipe when the output is redirected. This can cause the application to crash unexpectedly. To improve robustness, it's safer to usestd::io::stderr()withwriteln!and handle theResult, even if it's just by ignoring it.