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}