use core::time::Duration;
use reqwest;
use crate::error::Error;
use super::builder::ClientBuilder;
use super::config::{ ClientConfig, ClientConfigFormer };
use super::sync::SyncClientBuilder;
#[ derive( Debug, Clone ) ]
#[ allow( clippy::struct_excessive_bools ) ] pub struct Client
{
pub( crate ) api_key : String,
pub( crate ) base_url : String,
pub( crate ) http : reqwest::Client,
pub( crate ) timeout : Duration,
#[ cfg( feature = "retry" ) ]
pub( crate ) max_retries : u32,
#[ cfg( feature = "retry" ) ]
pub( crate ) base_delay : Duration,
#[ cfg( feature = "retry" ) ]
pub( crate ) max_delay : Duration,
#[ cfg( feature = "retry" ) ]
pub( crate ) enable_jitter : bool,
#[ cfg( feature = "retry" ) ]
#[ allow( dead_code ) ]
pub( crate ) request_timeout : Option< Duration >,
#[ cfg( feature = "retry" ) ]
pub( crate ) backoff_multiplier : f64,
#[ cfg( feature = "retry" ) ]
#[ allow( dead_code ) ]
pub( crate ) enable_retry_metrics : bool,
#[ cfg( feature = "retry" ) ]
pub( crate ) max_elapsed_time : Option< Duration >,
#[ cfg( feature = "circuit_breaker" ) ]
#[ allow( dead_code ) ]
pub( crate ) enable_circuit_breaker : bool,
#[ cfg( feature = "circuit_breaker" ) ]
#[ allow( dead_code ) ]
pub( crate ) circuit_breaker_failure_threshold : u32,
#[ cfg( feature = "circuit_breaker" ) ]
#[ allow( dead_code ) ]
pub( crate ) circuit_breaker_success_threshold : u32,
#[ cfg( feature = "circuit_breaker" ) ]
#[ allow( dead_code ) ]
pub( crate ) circuit_breaker_timeout : Duration,
#[ cfg( feature = "circuit_breaker" ) ]
#[ allow( dead_code ) ]
pub( crate ) enable_circuit_breaker_metrics : bool,
#[ cfg( feature = "circuit_breaker" ) ]
#[ allow( dead_code ) ]
pub( crate ) circuit_breaker_shared_state : bool,
#[ cfg( feature = "caching" ) ]
#[ allow( dead_code ) ]
pub( crate ) enable_request_cache : bool,
#[ cfg( feature = "caching" ) ]
#[ allow( dead_code ) ]
pub( crate ) cache_ttl : Duration,
#[ cfg( feature = "caching" ) ]
#[ allow( dead_code ) ]
pub( crate ) cache_max_size : usize,
#[ cfg( feature = "caching" ) ]
#[ allow( dead_code ) ]
pub( crate ) enable_cache_metrics : bool,
#[ cfg( feature = "caching" ) ]
pub( crate ) request_cache : Option< std::sync::Arc< crate::internal::http::RequestCache > >,
#[ cfg( feature = "rate_limiting" ) ]
#[ allow( dead_code ) ]
pub( crate ) enable_rate_limiting : bool,
#[ cfg( feature = "rate_limiting" ) ]
#[ allow( dead_code ) ]
pub( crate ) rate_limit_requests_per_second : f64,
#[ cfg( feature = "rate_limiting" ) ]
#[ allow( dead_code ) ]
pub( crate ) rate_limit_algorithm : String,
#[ cfg( feature = "rate_limiting" ) ]
#[ allow( dead_code ) ]
pub( crate ) rate_limit_bucket_size : usize,
#[ cfg( feature = "rate_limiting" ) ]
#[ allow( dead_code ) ]
pub( crate ) enable_rate_limiting_metrics : bool,
#[ cfg( feature = "compression" ) ]
pub( crate ) compression_config : Option< crate::internal::http::compression::CompressionConfig >,
}
impl Client
{
#[ must_use ]
#[ inline ]
pub fn former() -> ClientConfigFormer
{
ClientConfig::former()
}
#[ must_use ]
#[ inline ]
pub fn builder() -> ClientBuilder
{
ClientBuilder::new()
}
#[ must_use ]
#[ inline ]
pub fn sync_builder() -> SyncClientBuilder
{
SyncClientBuilder::new()
}
#[ inline ]
pub fn new() -> Result< Client, Error >
{
let api_key = match Self::load_api_key_from_secret_file()
{
Ok( key ) => key,
Err( secret_err ) => {
match std::env::var( "GEMINI_API_KEY" )
{
Ok( key ) if !key.is_empty() => key,
_ => {
return Err( Error::AuthenticationError(
format!(
"GEMINI_API_KEY not found. Tried:\n \
1. Workspace secrets : secret/-secrets.sh ({})\n \
2. Environment variable : GEMINI_API_KEY (not set or empty)\n\n \
Setup instructions:\n \
- Add to workspace secrets : echo 'export GEMINI_API_KEY=\"your-key\"' > > secret/-secrets.sh\n \
- Or set environment : export GEMINI_API_KEY=\"your-key\"\n \
- Note : workspace_tools 0.6.0 uses secret/ (visible directory, NO dot prefix)\n \
- See tests/readme.md for detailed setup guide",
secret_err
)
) );
}
}
}
};
Self::builder()
.api_key( api_key )
.build()
}
fn load_api_key_from_secret_file() -> Result< String, Error >
{
use workspace_tools as workspace;
let ws = workspace::workspace()
.map_err( | e | Error::Io( format!( "Failed to resolve workspace : {e}" ) ) )?;
let api_key = ws.load_secret_key( "GEMINI_API_KEY", "-secrets.sh" )
.map_err( | e | Error::AuthenticationError( format!( "key not found or file unreadable : {e}" ) ) )?;
Ok( api_key )
}
#[ inline ]
pub async fn send_get_request( &self, url : &str ) -> Result< reqwest::Response, Error >
{
let url_with_key = self.add_api_key_to_url( url );
let response = self.http
.get( &url_with_key )
.header( "Content-Type", "application/json" )
.send()
.await?;
Ok( response )
}
#[ inline ]
pub async fn send_post_request( &self, url : &str, body : &serde_json::Value ) -> Result< reqwest::Response, Error >
{
let url_with_key = self.add_api_key_to_url( url );
let json_body = self.serialize_request_body( body )?;
let response = self.http
.post( &url_with_key )
.header( "Content-Type", "application/json" )
.body( json_body )
.send()
.await?;
Ok( response )
}
#[ inline ]
pub fn serialize_request_body( &self, body : &serde_json::Value ) -> Result< String, Error >
{
serde_json ::to_string( body )
.map_err( | e | Error::SerializationError( format!( "Failed to serialize request body : {e}" ) ) )
}
#[ inline ]
pub fn deserialize_response< T >( &self, response_text : &str ) -> Result< T, Error >
where
T : for< 'de > serde::Deserialize< 'de >,
{
serde_json ::from_str( response_text )
.map_err( | e | Error::DeserializationError( format!( "Failed to deserialize response : {e}" ) ) )
}
#[ must_use ]
#[ inline ]
pub fn add_api_key_to_url( &self, base_url : &str ) -> String
{
if base_url.contains( '?' )
{
{ let encoded_key = urlencoding::encode( &self.api_key ); format!( "{base_url}&key={encoded_key}" ) }
}
else
{
{ let encoded_key = urlencoding::encode( &self.api_key ); format!( "{base_url}?key={encoded_key}" ) }
}
}
#[ inline ]
pub fn handle_response_error( &self, status : u16, status_text : &str, message : &str ) -> Result< (), Error >
{
let error_message = if message.is_empty()
{
format!( "{status} {status_text}" )
}
else
{
format!( "{status} {status_text}: {message}" )
};
match status
{
500..=599 => Err( Error::ServerError( error_message ) ),
_ => Err( Error::ApiError( error_message ) ),
}
}
#[ cfg( feature = "retry" ) ]
pub( crate ) fn to_retry_config( &self ) -> Option< crate::internal::http::RetryConfig >
{
if self.max_retries == 0
{
None
} else {
Some( crate::internal::http::RetryConfig {
max_retries : self.max_retries,
base_delay : self.base_delay,
max_delay : self.max_delay,
backoff_multiplier : self.backoff_multiplier,
enable_jitter : self.enable_jitter,
max_elapsed_time : self.max_elapsed_time,
} )
}
}
#[ cfg( feature = "circuit_breaker" ) ]
pub( crate ) fn to_circuit_breaker_config( &self ) -> Option< crate::internal::http::CircuitBreakerConfig >
{
if self.circuit_breaker_failure_threshold == 0
{
None
} else {
Some( crate::internal::http::CircuitBreakerConfig {
failure_threshold : self.circuit_breaker_failure_threshold,
timeout : self.circuit_breaker_timeout,
success_threshold : self.circuit_breaker_success_threshold,
enable_metrics : self.enable_circuit_breaker_metrics,
} )
}
}
#[ cfg( feature = "rate_limiting" ) ]
pub( crate ) fn to_rate_limiting_config( &self ) -> Option< crate::internal::http::RateLimitingConfig >
{
if self.rate_limit_requests_per_second <= 0.0
{
None
} else {
Some( crate::internal::http::RateLimitingConfig {
requests_per_second : self.rate_limit_requests_per_second,
bucket_size : self.rate_limit_bucket_size,
algorithm : self.rate_limit_algorithm.clone(),
enable_metrics : self.enable_rate_limiting_metrics,
} )
}
}
}