use crate::error::{RainyError, Result};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
use secrecy::{ExposeSecret, SecretString};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct AuthConfig {
pub api_key: SecretString,
pub base_url: String,
pub timeout_seconds: u64,
pub max_retries: u32,
pub enable_retry: bool,
pub user_agent: String,
}
impl AuthConfig {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: SecretString::from(api_key.into()),
base_url: crate::DEFAULT_BASE_URL.to_string(),
timeout_seconds: 30,
max_retries: 3,
enable_retry: true,
user_agent: format!("rainy-sdk/{}", crate::VERSION),
}
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
pub fn with_timeout(mut self, seconds: u64) -> Self {
self.timeout_seconds = seconds;
self
}
pub fn with_max_retries(mut self, retries: u32) -> Self {
self.max_retries = retries;
self
}
pub fn with_retry(mut self, enable: bool) -> Self {
self.enable_retry = enable;
self
}
pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = user_agent.into();
self
}
pub fn validate(&self) -> Result<()> {
if self.api_key.expose_secret().is_empty() {
return Err(RainyError::Authentication {
code: "EMPTY_API_KEY".to_string(),
message: "API key cannot be empty".to_string(),
retryable: false,
});
}
let key = self.api_key.expose_secret();
if key.starts_with("ra-cowork") {
if key.len() != 57 {
return Err(RainyError::Authentication {
code: "INVALID_COWORK_API_KEY_FORMAT".to_string(),
message: "Cowork API key must be 57 characters (ra-cowork + 48 hex)"
.to_string(),
retryable: false,
});
}
} else if key.starts_with("ra-") {
if key.len() != 51 {
return Err(RainyError::Authentication {
code: "INVALID_API_KEY_FORMAT".to_string(),
message: "Standard API key must be 51 characters (ra- + 48 hex)".to_string(),
retryable: false,
});
}
} else {
return Err(RainyError::Authentication {
code: "INVALID_API_KEY_FORMAT".to_string(),
message: "API key must start with 'ra-' or 'ra-cowork'".to_string(),
retryable: false,
});
}
if url::Url::parse(&self.base_url).is_err() {
return Err(RainyError::InvalidRequest {
code: "INVALID_BASE_URL".to_string(),
message: "Base URL is not a valid URL".to_string(),
details: None,
});
}
Ok(())
}
pub fn is_cowork_key(&self) -> bool {
self.api_key.expose_secret().starts_with("ra-cowork")
}
pub fn build_headers(&self) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_str(&self.user_agent)?);
headers.insert(
reqwest::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
let auth_value = format!("Bearer {}", self.api_key.expose_secret());
headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);
Ok(headers)
}
pub fn timeout(&self) -> Duration {
Duration::from_secs(self.timeout_seconds)
}
}
impl std::fmt::Display for AuthConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"AuthConfig {{ base_url: {}, timeout: {}s, retries: {} }}",
self.base_url, self.timeout_seconds, self.max_retries
)
}
}
#[deprecated(note = "Use the governor-based rate limiting in RainyClient instead")]
#[derive(Debug)]
pub struct RateLimiter {
requests_per_minute: u32,
last_request: std::time::Instant,
request_count: u32,
}
#[allow(deprecated)]
impl RateLimiter {
pub fn new(requests_per_minute: u32) -> Self {
Self {
requests_per_minute,
last_request: std::time::Instant::now(),
request_count: 0,
}
}
pub async fn wait_if_needed(&mut self) -> Result<()> {
let now = std::time::Instant::now();
let elapsed = now.duration_since(self.last_request);
if elapsed >= Duration::from_secs(60) {
self.request_count = 0;
self.last_request = now;
}
if self.request_count >= self.requests_per_minute {
let wait_time = Duration::from_secs(60) - elapsed;
tokio::time::sleep(wait_time).await;
self.request_count = 0;
self.last_request = std::time::Instant::now();
}
self.request_count += 1;
Ok(())
}
}