lastfm_client/
config.rs

1use crate::error::{LastFmError, Result};
2use std::env;
3use std::time::Duration;
4
5/// Rate limiting configuration
6#[derive(Debug, Clone)]
7pub struct RateLimit {
8    pub max_requests: u32,
9    pub per_duration: Duration,
10}
11
12/// Client configuration
13#[derive(Debug, Clone)]
14pub struct Config {
15    pub(crate) api_key: String,
16    pub(crate) user_agent: String,
17    pub(crate) timeout: Duration,
18    pub(crate) max_concurrent_requests: usize,
19    pub(crate) retry_attempts: u32,
20    pub(crate) rate_limit: Option<RateLimit>,
21}
22
23impl Config {
24    /// Get the API key
25    #[must_use]
26    pub fn api_key(&self) -> &str {
27        &self.api_key
28    }
29
30    /// Get the user agent
31    #[must_use]
32    pub fn user_agent(&self) -> &str {
33        &self.user_agent
34    }
35
36    /// Get the timeout duration
37    #[must_use]
38    pub fn timeout(&self) -> Duration {
39        self.timeout
40    }
41
42    /// Get maximum concurrent requests
43    #[must_use]
44    pub fn max_concurrent_requests(&self) -> usize {
45        self.max_concurrent_requests
46    }
47
48    /// Get retry attempts
49    #[must_use]
50    pub fn retry_attempts(&self) -> u32 {
51        self.retry_attempts
52    }
53
54    /// Get rate limit configuration
55    #[must_use]
56    pub fn rate_limit(&self) -> Option<&RateLimit> {
57        self.rate_limit.as_ref()
58    }
59}
60
61/// Configuration builder for creating a `Config` instance
62#[derive(Debug, Default)]
63pub struct ConfigBuilder {
64    api_key: Option<String>,
65    user_agent: Option<String>,
66    timeout: Option<Duration>,
67    max_concurrent_requests: Option<usize>,
68    retry_attempts: Option<u32>,
69    rate_limit: Option<RateLimit>,
70}
71
72impl ConfigBuilder {
73    /// Create a new configuration builder
74    #[must_use]
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Set the API key
80    ///
81    /// # Example
82    /// ```
83    /// use lastfm_client::ConfigBuilder;
84    ///
85    /// let builder = ConfigBuilder::new().api_key("my_api_key");
86    /// ```
87    #[must_use]
88    pub fn api_key(mut self, key: impl Into<String>) -> Self {
89        self.api_key = Some(key.into());
90        self
91    }
92
93    /// Load API key from environment variable
94    ///
95    /// # Example
96    /// ```no_run
97    /// use lastfm_client::ConfigBuilder;
98    ///
99    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
100    /// let builder = ConfigBuilder::new().from_env()?;
101    /// # Ok(())
102    /// # }
103    /// ```
104    /// # Errors
105    /// Returns an error if the `LAST_FM_API_KEY` environment variable is missing.
106    pub fn from_env(mut self) -> Result<Self> {
107        let api_key = env::var("LAST_FM_API_KEY")
108            .map_err(|_| LastFmError::MissingEnvVar("LAST_FM_API_KEY".to_string()))?;
109        self.api_key = Some(api_key);
110        Ok(self)
111    }
112
113    /// Set the user agent
114    ///
115    /// If not set, defaults to `async_lastfm/VERSION`
116    #[must_use]
117    pub fn user_agent(mut self, agent: impl Into<String>) -> Self {
118        self.user_agent = Some(agent.into());
119        self
120    }
121
122    /// Set the request timeout
123    ///
124    /// If not set, defaults to 30 seconds
125    #[must_use]
126    pub fn timeout(mut self, duration: Duration) -> Self {
127        self.timeout = Some(duration);
128        self
129    }
130
131    /// Set maximum concurrent requests
132    ///
133    /// If not set, defaults to 5
134    #[must_use]
135    pub fn max_concurrent_requests(mut self, max: usize) -> Self {
136        self.max_concurrent_requests = Some(max);
137        self
138    }
139
140    /// Set number of retry attempts
141    ///
142    /// If not set, defaults to 3
143    #[must_use]
144    pub fn retry_attempts(mut self, attempts: u32) -> Self {
145        self.retry_attempts = Some(attempts);
146        self
147    }
148
149    /// Set rate limiting
150    ///
151    /// # Example
152    /// ```
153    /// use lastfm_client::ConfigBuilder;
154    /// use std::time::Duration;
155    ///
156    /// let builder = ConfigBuilder::new()
157    ///     .api_key("key")
158    ///     .rate_limit(5, Duration::from_secs(1)); // Max 5 requests per second
159    /// ```
160    #[must_use]
161    pub fn rate_limit(mut self, max_requests: u32, per_duration: Duration) -> Self {
162        self.rate_limit = Some(RateLimit {
163            max_requests,
164            per_duration,
165        });
166        self
167    }
168
169    /// Build the configuration
170    ///
171    /// # Errors
172    /// Returns an error if the API key is not set and cannot be loaded from environment
173    pub fn build(self) -> Result<Config> {
174        // Try to get API key from builder, then from environment
175        let api_key = self.api_key.or_else(|| {
176            env::var("LAST_FM_API_KEY").ok()
177        }).ok_or_else(|| LastFmError::Config(
178            "API key is required. Set it via .api_key() or LAST_FM_API_KEY environment variable".to_string()
179        ))?;
180
181        Ok(Config {
182            api_key,
183            user_agent: self
184                .user_agent
185                .unwrap_or_else(|| format!("async_lastfm/{}", env!("CARGO_PKG_VERSION"))),
186            timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
187            max_concurrent_requests: self.max_concurrent_requests.unwrap_or(5),
188            retry_attempts: self.retry_attempts.unwrap_or(3),
189            rate_limit: self.rate_limit,
190        })
191    }
192
193    /// Build the configuration with defaults, trying to load API key from environment
194    ///
195    /// This is equivalent to `ConfigBuilder::new().build()` but more explicit about
196    /// the default behavior.
197    ///
198    /// # Errors
199    /// Returns an error if the API key is not set and cannot be loaded from environment
200    pub fn build_with_defaults() -> Result<Config> {
201        Self::new().build()
202    }
203}
204
205/// Validates that all required environment variables are set
206///
207/// # Errors
208/// Returns `LastFmError::MissingEnvVar` if any required environment variable is missing
209pub fn validate_env_vars() -> Result<()> {
210    const REQUIRED_ENV_VARS: &[&str] = &["LAST_FM_API_KEY"];
211
212    let mut missing_vars = Vec::new();
213
214    for var_name in REQUIRED_ENV_VARS {
215        if env::var(var_name).is_err() {
216            missing_vars.push(*var_name);
217        }
218    }
219
220    if !missing_vars.is_empty() {
221        return Err(LastFmError::MissingEnvVar(missing_vars.join(", ")));
222    }
223
224    Ok(())
225}
226
227/// Gets a required environment variable
228///
229/// # Errors
230/// Returns `LastFmError::MissingEnvVar` if the environment variable is not set
231pub fn get_required_env_var(var_name: &str) -> Result<String> {
232    env::var(var_name).map_err(|_| LastFmError::MissingEnvVar(var_name.to_string()))
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_config_builder() {
241        let config = ConfigBuilder::new()
242            .api_key("test_key")
243            .user_agent("test_agent")
244            .timeout(Duration::from_secs(60))
245            .max_concurrent_requests(10)
246            .retry_attempts(5)
247            .build()
248            .unwrap();
249
250        assert_eq!(config.api_key(), "test_key");
251        assert_eq!(config.user_agent(), "test_agent");
252        assert_eq!(config.timeout(), Duration::from_secs(60));
253        assert_eq!(config.max_concurrent_requests(), 10);
254        assert_eq!(config.retry_attempts(), 5);
255    }
256
257    #[test]
258    fn test_config_builder_defaults() {
259        let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
260
261        assert_eq!(config.api_key(), "test_key");
262        assert!(config.user_agent().starts_with("async_lastfm/"));
263        assert_eq!(config.timeout(), Duration::from_secs(30));
264        assert_eq!(config.max_concurrent_requests(), 5);
265        assert_eq!(config.retry_attempts(), 3);
266    }
267
268    #[test]
269    fn test_config_builder_missing_api_key() {
270        let result = ConfigBuilder::new().build();
271        assert!(result.is_err());
272        assert!(matches!(result.unwrap_err(), LastFmError::Config(_)));
273    }
274
275    #[test]
276    fn test_rate_limit() {
277        let config = ConfigBuilder::new()
278            .api_key("test_key")
279            .rate_limit(10, Duration::from_secs(1))
280            .build()
281            .unwrap();
282
283        let rate_limit = config.rate_limit().unwrap();
284        assert_eq!(rate_limit.max_requests, 10);
285        assert_eq!(rate_limit.per_duration, Duration::from_secs(1));
286    }
287}