rainy_sdk/
auth.rs

1use crate::error::{RainyError, Result};
2use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
3use secrecy::{ExposeSecret, Secret};
4use std::time::Duration;
5
6/// Configuration for authentication and client behavior.
7///
8/// `AuthConfig` holds all the necessary information for authenticating with the Rainy API,
9/// as well as settings for request behavior like timeouts and retries.
10///
11/// # Examples
12///
13/// ```rust
14/// use rainy_sdk::auth::AuthConfig;
15///
16/// let config = AuthConfig::new("your-api-key")
17///     .with_base_url("https://api.example.com")
18///     .with_timeout(60)
19///     .with_max_retries(5);
20///
21/// assert_eq!(config.base_url, "https://api.example.com");
22/// assert_eq!(config.timeout_seconds, 60);
23/// assert_eq!(config.max_retries, 5);
24/// ```
25#[derive(Debug, Clone)]
26pub struct AuthConfig {
27    /// The API key used for authenticating with the Rainy API.
28    pub api_key: Secret<String>,
29
30    /// The base URL of the Rainy API. Defaults to the official endpoint.
31    pub base_url: String,
32
33    /// The timeout for HTTP requests, in seconds.
34    pub timeout_seconds: u64,
35
36    /// The maximum number of times to retry a failed request.
37    pub max_retries: u32,
38
39    /// A flag to enable or disable automatic retries with exponential backoff.
40    pub enable_retry: bool,
41
42    /// The user agent string to send with each request.
43    pub user_agent: String,
44}
45
46impl AuthConfig {
47    /// Creates a new `AuthConfig` with the given API key and default settings.
48    ///
49    /// # Arguments
50    ///
51    /// * `api_key` - Your Rainy API key.
52    pub fn new(api_key: impl Into<String>) -> Self {
53        Self {
54            api_key: Secret::new(api_key.into()),
55            base_url: crate::DEFAULT_BASE_URL.to_string(),
56            timeout_seconds: 30,
57            max_retries: 3,
58            enable_retry: true,
59            user_agent: format!("rainy-sdk/{}", crate::VERSION),
60        }
61    }
62
63    /// Sets a custom base URL for the API.
64    ///
65    /// # Arguments
66    ///
67    /// * `base_url` - The new base URL to use.
68    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
69        self.base_url = base_url.into();
70        self
71    }
72
73    /// Sets a custom timeout for HTTP requests.
74    ///
75    /// # Arguments
76    ///
77    /// * `seconds` - The timeout duration in seconds.
78    pub fn with_timeout(mut self, seconds: u64) -> Self {
79        self.timeout_seconds = seconds;
80        self
81    }
82
83    /// Sets the maximum number of retry attempts for failed requests.
84    ///
85    /// # Arguments
86    ///
87    /// * `retries` - The maximum number of retries.
88    pub fn with_max_retries(mut self, retries: u32) -> Self {
89        self.max_retries = retries;
90        self
91    }
92
93    /// Enables or disables automatic retries.
94    ///
95    /// # Arguments
96    ///
97    /// * `enable` - `true` to enable retries, `false` to disable.
98    pub fn with_retry(mut self, enable: bool) -> Self {
99        self.enable_retry = enable;
100        self
101    }
102
103    /// Sets a custom user agent string for requests.
104    ///
105    /// # Arguments
106    ///
107    /// * `user_agent` - The new user agent string.
108    pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
109        self.user_agent = user_agent.into();
110        self
111    }
112
113    /// Validates the `AuthConfig` settings.
114    ///
115    /// This method checks for common configuration errors, such as an empty API key
116    /// or an invalid base URL.
117    ///
118    /// # Returns
119    ///
120    /// A `Result` that is `Ok(())` if the configuration is valid, or a `RainyError` if it's not.
121    pub fn validate(&self) -> Result<()> {
122        if self.api_key.expose_secret().is_empty() {
123            return Err(RainyError::Authentication {
124                code: "EMPTY_API_KEY".to_string(),
125                message: "API key cannot be empty".to_string(),
126                retryable: false,
127            });
128        }
129
130        // Basic API key format validation (starts with 'ra-')
131        if !self.api_key.expose_secret().starts_with("ra-") {
132            return Err(RainyError::Authentication {
133                code: "INVALID_API_KEY_FORMAT".to_string(),
134                message: "API key must start with 'ra-'".to_string(),
135                retryable: false,
136            });
137        }
138
139        // Validate URL format
140        if url::Url::parse(&self.base_url).is_err() {
141            return Err(RainyError::InvalidRequest {
142                code: "INVALID_BASE_URL".to_string(),
143                message: "Base URL is not a valid URL".to_string(),
144                details: None,
145            });
146        }
147
148        Ok(())
149    }
150
151    /// Builds the necessary HTTP headers for an API request.
152    ///
153    /// This method constructs a `HeaderMap` containing the `Authorization` and `User-Agent`
154    /// headers based on the `AuthConfig`.
155    ///
156    /// # Returns
157    ///
158    /// A `Result` containing the `HeaderMap` or a `RainyError` if header creation fails.
159    pub fn build_headers(&self) -> Result<HeaderMap> {
160        let mut headers = HeaderMap::new();
161
162        // Set User-Agent
163        headers.insert(USER_AGENT, HeaderValue::from_str(&self.user_agent)?);
164
165        // Set Content-Type for JSON requests
166        headers.insert(
167            reqwest::header::CONTENT_TYPE,
168            HeaderValue::from_static("application/json"),
169        );
170
171        // Set authorization header
172        let auth_value = format!("Bearer {}", self.api_key.expose_secret());
173        headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);
174
175        Ok(headers)
176    }
177
178    /// Returns the request timeout as a `Duration`.
179    pub fn timeout(&self) -> Duration {
180        Duration::from_secs(self.timeout_seconds)
181    }
182}
183
184impl std::fmt::Display for AuthConfig {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        write!(
187            f,
188            "AuthConfig {{ base_url: {}, timeout: {}s, retries: {} }}",
189            self.base_url, self.timeout_seconds, self.max_retries
190        )
191    }
192}
193
194/// A simple rate limiter.
195///
196/// This rate limiter is deprecated and should not be used in new code.
197/// The `RainyClient` now uses a more robust, feature-flagged rate limiting mechanism
198/// based on the `governor` crate.
199#[deprecated(note = "Use the governor-based rate limiting in RainyClient instead")]
200#[derive(Debug)]
201pub struct RateLimiter {
202    requests_per_minute: u32,
203    last_request: std::time::Instant,
204    request_count: u32,
205}
206
207#[allow(deprecated)]
208impl RateLimiter {
209    /// Creates a new `RateLimiter`.
210    ///
211    /// # Arguments
212    ///
213    /// * `requests_per_minute` - The maximum number of requests allowed per minute.
214    pub fn new(requests_per_minute: u32) -> Self {
215        Self {
216            requests_per_minute,
217            last_request: std::time::Instant::now(),
218            request_count: 0,
219        }
220    }
221
222    /// Pauses execution if the rate limit has been exceeded.
223    ///
224    /// This method will asynchronously wait until the next request can be sent without
225    /// violating the rate limit.
226    pub async fn wait_if_needed(&mut self) -> Result<()> {
227        let now = std::time::Instant::now();
228        let elapsed = now.duration_since(self.last_request);
229
230        // Reset counter if a minute has passed
231        if elapsed >= Duration::from_secs(60) {
232            self.request_count = 0;
233            self.last_request = now;
234        }
235
236        // Check if we've exceeded the rate limit
237        if self.request_count >= self.requests_per_minute {
238            let wait_time = Duration::from_secs(60) - elapsed;
239            tokio::time::sleep(wait_time).await;
240            self.request_count = 0;
241            self.last_request = std::time::Instant::now();
242        }
243
244        self.request_count += 1;
245        Ok(())
246    }
247}