Skip to main content

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