1use crate::error::{LastFmError, Result};
2use std::env;
3use std::time::Duration;
4
5#[derive(Debug, Clone)]
7#[non_exhaustive]
8pub struct RateLimit {
9 pub max_requests: u32,
11 pub per_duration: Duration,
13}
14
15#[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 #[must_use]
29 pub fn api_key(&self) -> &str {
30 &self.api_key
31 }
32
33 #[must_use]
35 pub fn user_agent(&self) -> &str {
36 &self.user_agent
37 }
38
39 #[must_use]
41 pub const fn timeout(&self) -> Duration {
42 self.timeout
43 }
44
45 #[must_use]
47 pub const fn max_concurrent_requests(&self) -> usize {
48 self.max_concurrent_requests
49 }
50
51 #[must_use]
53 pub const fn retry_attempts(&self) -> u32 {
54 self.retry_attempts
55 }
56
57 #[must_use]
59 pub const fn rate_limit(&self) -> Option<&RateLimit> {
60 self.rate_limit.as_ref()
61 }
62}
63
64#[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 #[must_use]
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 #[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 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 #[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 #[must_use]
129 pub const fn timeout(mut self, duration: Duration) -> Self {
130 self.timeout = Some(duration);
131 self
132 }
133
134 #[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 #[must_use]
147 pub const fn retry_attempts(mut self, attempts: u32) -> Self {
148 self.retry_attempts = Some(attempts);
149 self
150 }
151
152 #[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 pub fn build(self) -> Result<Config> {
177 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 pub fn build_with_defaults() -> Result<Config> {
204 Self::new().build()
205 }
206}
207
208pub 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
230pub 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}