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
35 changes: 34 additions & 1 deletion src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,27 @@ pub struct JsonSchemaProperty {
pub additional_properties: Option<Box<JsonSchemaProperty>>,
}

/// Cached custom API base URL, read once from the environment.
static CUSTOM_API_BASE_URL: std::sync::LazyLock<Option<String>> =
std::sync::LazyLock::new(|| std::env::var("GWS_API_BASE_URL").ok().filter(|s| !s.is_empty()));

/// Returns the custom API base URL override, if set.
///
/// When `GWS_API_BASE_URL` is set (e.g., `http://localhost:8099`), all API
/// requests are directed to this endpoint instead of the real Google APIs.
/// Authentication is skipped automatically. This is useful for testing against
/// mock API servers.
pub fn custom_api_base_url() -> Option<&'static str> {
CUSTOM_API_BASE_URL.as_deref()
}
Comment on lines +190 to +198
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better performance and to ensure consistency, it's a good practice to read the GWS_API_BASE_URL environment variable only once. You can use the once_cell crate to achieve this with a Lazy static. This avoids repeated environment variable lookups on every call.

You'll need to add once_cell = "1" to your Cargo.toml dependencies.

Suggested change
/// Returns the custom API base URL override, if set.
///
/// When `GWS_API_BASE_URL` is set (e.g., `http://localhost:8099`), all API
/// requests are directed to this endpoint instead of the real Google APIs.
/// Authentication is skipped automatically. This is useful for testing against
/// mock API servers.
pub fn custom_api_base_url() -> Option<String> {
std::env::var("GWS_API_BASE_URL").ok().filter(|s| !s.is_empty())
}
use once_cell::sync::Lazy; // Add to file imports
static GWS_API_BASE_URL: Lazy<Option<String>> =
Lazy::new(|| std::env::var("GWS_API_BASE_URL").ok().filter(|s| !s.is_empty()));
/// Returns the custom API base URL override, if set.
///
/// When `GWS_API_BASE_URL` is set (e.g., `http://localhost:8099`), all API
/// requests are directed to this endpoint instead of the real Google APIs.
/// Authentication is skipped automatically. This is useful for testing against
/// mock API servers.
pub fn custom_api_base_url() -> Option<String> {
GWS_API_BASE_URL.as_ref().cloned()
}


/// Fetches and caches a Google Discovery Document.
///
/// The Discovery Document is always fetched from the real Google APIs so that
/// gws knows the full command structure (resources, methods, parameters). When
/// `GWS_API_BASE_URL` is set, the document's `root_url` and `base_url` are
/// rewritten to point at the custom endpoint — actual API requests then go to
/// the mock server while the CLI command tree remains fully functional.
pub async fn fetch_discovery_document(
service: &str,
version: &str,
Expand Down Expand Up @@ -235,7 +255,20 @@ 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)?;

// When a custom API base URL is set, rewrite the Discovery Document's
// root_url and base_url so that all HTTP requests go to the custom
// endpoint (e.g., a mock server) instead of the real Google APIs.
if let Some(base) = custom_api_base_url() {
let base_trimmed = base.trim_end_matches('/');
doc.root_url = format!("{base_trimmed}/");
doc.base_url = Some(format!(
"{base_trimmed}/{}/{}/",
doc.name, doc.version
));
}

Ok(doc)
}

Expand Down
15 changes: 15 additions & 0 deletions src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ pub enum AuthMethod {
None,
}

/// Resolve authentication, skipping OAuth when a custom API endpoint is set.
///
/// When `GWS_API_BASE_URL` is set, all requests go to a mock server that
/// doesn't need (or support) Google OAuth. This helper centralises that check
/// so every call-site doesn't need to know about the env var.
pub async fn resolve_auth(scopes: &[&str]) -> (Option<String>, AuthMethod) {
if crate::discovery::custom_api_base_url().is_some() {
return (None, AuthMethod::None);
}
match crate::auth::get_token(scopes).await {
Ok(t) => (Some(t), AuthMethod::OAuth),
Err(_) => (None, AuthMethod::None),
}
}

/// Configuration for auto-pagination.
#[derive(Debug, Clone)]
pub struct PaginationConfig {
Expand Down
7 changes: 2 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,8 @@ async fn run() -> Result<(), GwsError> {
// Get scopes from the method
let scopes: Vec<&str> = method.scopes.iter().map(|s| s.as_str()).collect();

// Authenticate: try OAuth, otherwise proceed unauthenticated
let (token, auth_method) = match auth::get_token(&scopes).await {
Ok(t) => (Some(t), executor::AuthMethod::OAuth),
Err(_) => (None, executor::AuthMethod::None),
};
// Authenticate: skips OAuth automatically when GWS_API_BASE_URL is set.
let (token, auth_method) = executor::resolve_auth(&scopes).await;

// Execute
executor::execute_method(
Expand Down
5 changes: 1 addition & 4 deletions src/mcp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,10 +407,7 @@ async fn handle_tools_call(params: &Value, config: &ServerConfig) -> Result<Valu
};

let scopes: Vec<&str> = method.scopes.iter().map(|s| s.as_str()).collect();
let (token, auth_method) = match crate::auth::get_token(&scopes).await {
Ok(t) => (Some(t), crate::executor::AuthMethod::OAuth),
Err(_) => (None, crate::executor::AuthMethod::None),
};
let (token, auth_method) = crate::executor::resolve_auth(&scopes).await;

let result = crate::executor::execute_method(
&doc,
Expand Down