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}