Skip to main content

rainy_sdk/
auth.rs

1use crate::error::{RainyError, Result};
2use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
3use secrecy::{ExposeSecret, SecretString};
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: SecretString,
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: SecretString::from(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    /// Supports standard API key format: `ra-{48 hex}` = 51 characters.
119    ///
120    /// # Returns
121    ///
122    /// A `Result` that is `Ok(())` if the configuration is valid, or a `RainyError` if it's not.
123    pub fn validate(&self) -> Result<()> {
124        if self.api_key.expose_secret().is_empty() {
125            return Err(RainyError::Authentication {
126                code: "EMPTY_API_KEY".to_string(),
127                message: "API key cannot be empty".to_string(),
128                retryable: false,
129            });
130        }
131
132        let key = self.api_key.expose_secret();
133
134        if key.starts_with("ra-") {
135            // Standard key: ra- (3 chars) + 48 hex = 51 chars
136            if key.len() != 51 {
137                return Err(RainyError::Authentication {
138                    code: "INVALID_API_KEY_FORMAT".to_string(),
139                    message: "Standard API key must be 51 characters (ra- + 48 hex)".to_string(),
140                    retryable: false,
141                });
142            }
143        } else {
144            return Err(RainyError::Authentication {
145                code: "INVALID_API_KEY_FORMAT".to_string(),
146                message: "API key must start with 'ra-'".to_string(),
147                retryable: false,
148            });
149        }
150
151        // Validate URL format
152        if url::Url::parse(&self.base_url).is_err() {
153            return Err(RainyError::InvalidRequest {
154                code: "INVALID_BASE_URL".to_string(),
155                message: "Base URL is not a valid URL".to_string(),
156                details: None,
157            });
158        }
159
160        Ok(())
161    }
162
163    /// Builds the necessary HTTP headers for an API request.
164    ///
165    /// This method constructs a `HeaderMap` containing the `Authorization` and `User-Agent`
166    /// headers based on the `AuthConfig`.
167    ///
168    /// # Returns
169    ///
170    /// A `Result` containing the `HeaderMap` or a `RainyError` if header creation fails.
171    pub fn build_headers(&self) -> Result<HeaderMap> {
172        let mut headers = HeaderMap::new();
173
174        // Set User-Agent
175        headers.insert(USER_AGENT, HeaderValue::from_str(&self.user_agent)?);
176
177        // Set Content-Type for JSON requests
178        headers.insert(
179            reqwest::header::CONTENT_TYPE,
180            HeaderValue::from_static("application/json"),
181        );
182
183        // Set authorization header
184        let auth_value = format!("Bearer {}", self.api_key.expose_secret());
185        headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);
186
187        Ok(headers)
188    }
189
190    /// Returns the request timeout as a `Duration`.
191    pub fn timeout(&self) -> Duration {
192        Duration::from_secs(self.timeout_seconds)
193    }
194}
195
196impl std::fmt::Display for AuthConfig {
197    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198        write!(
199            f,
200            "AuthConfig {{ base_url: {}, timeout: {}s, retries: {} }}",
201            self.base_url, self.timeout_seconds, self.max_retries
202        )
203    }
204}
205
206/// A simple rate limiter.
207///
208/// This rate limiter is deprecated and should not be used in new code.
209/// The `RainyClient` now uses a more robust, feature-flagged rate limiting mechanism
210/// based on the `governor` crate.
211#[deprecated(note = "Use the governor-based rate limiting in RainyClient instead")]
212#[derive(Debug)]
213pub struct RateLimiter {
214    requests_per_minute: u32,
215    last_request: std::time::Instant,
216    request_count: u32,
217}
218
219#[allow(deprecated)]
220impl RateLimiter {
221    /// Creates a new `RateLimiter`.
222    ///
223    /// # Arguments
224    ///
225    /// * `requests_per_minute` - The maximum number of requests allowed per minute.
226    pub fn new(requests_per_minute: u32) -> Self {
227        Self {
228            requests_per_minute,
229            last_request: std::time::Instant::now(),
230            request_count: 0,
231        }
232    }
233
234    /// Pauses execution if the rate limit has been exceeded.
235    ///
236    /// This method will asynchronously wait until the next request can be sent without
237    /// violating the rate limit.
238    pub async fn wait_if_needed(&mut self) -> Result<()> {
239        let now = std::time::Instant::now();
240        let elapsed = now.duration_since(self.last_request);
241
242        // Reset counter if a minute has passed
243        if elapsed >= Duration::from_secs(60) {
244            self.request_count = 0;
245            self.last_request = now;
246        }
247
248        // Check if we've exceeded the rate limit
249        if self.request_count >= self.requests_per_minute {
250            let wait_time = Duration::from_secs(60) - elapsed;
251            tokio::time::sleep(wait_time).await;
252            self.request_count = 0;
253            self.last_request = std::time::Instant::now();
254        }
255
256        self.request_count += 1;
257        Ok(())
258    }
259}