lastfm_client/client/
lastfm.rs

1use crate::api::{LovedTracksClient, RecentTracksClient, TopTracksClient};
2use crate::client::{
3    HttpClient, RateLimitedClient, RateLimiter, ReqwestClient, RetryClient, RetryPolicy,
4};
5use crate::config::{Config, ConfigBuilder};
6use crate::error::Result;
7use std::sync::Arc;
8
9/// Main Last.fm API client
10///
11/// This is the entry point for interacting with the Last.fm API using the new v2.0 API.
12///
13/// # Example
14/// ```
15/// use lastfm_client::LastFmClient;
16/// use std::time::Duration;
17///
18/// // Create client with custom configuration
19/// let client = LastFmClient::builder()
20///     .api_key("your_api_key")
21///     .timeout(Duration::from_secs(60))
22///     .max_concurrent_requests(10)
23///     .build()
24///     .unwrap();
25///
26/// // Use client.recent_tracks() to fetch data
27/// ```
28pub struct LastFmClient {
29    config: Arc<Config>,
30    recent_tracks_client: RecentTracksClient,
31    loved_tracks_client: LovedTracksClient,
32    top_tracks_client: TopTracksClient,
33}
34
35impl LastFmClient {
36    /// Create a new configuration builder
37    ///
38    /// This is the recommended way to create a `LastFmClient`.
39    ///
40    /// # Example
41    /// ```no_run
42    /// use lastfm_client::LastFmClient;
43    ///
44    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
45    /// let client = LastFmClient::builder()
46    ///     .api_key("your_api_key")
47    ///     .build()?;
48    /// # Ok(())
49    /// # }
50    /// ```
51    #[must_use]
52    pub fn builder() -> ConfigBuilder {
53        ConfigBuilder::new()
54    }
55
56    /// Create a new `LastFmClient` with default configuration
57    ///
58    /// This will automatically try to load the API key from the `LAST_FM_API_KEY`
59    /// environment variable. All other settings use sensible defaults.
60    ///
61    /// # Example
62    /// ```no_run
63    /// use lastfm_client::LastFmClient;
64    ///
65    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
66    /// let client = LastFmClient::new()?;
67    /// # Ok(())
68    /// # }
69    /// ```
70    ///
71    /// # Errors
72    /// Returns an error if the API key is not set and cannot be loaded from environment
73    pub fn new() -> Result<Self> {
74        let config = ConfigBuilder::build_with_defaults()?;
75        Ok(Self::from_config(config))
76    }
77
78    /// Create a new `LastFmClient` from a configuration
79    ///
80    /// This automatically sets up retry logic and rate limiting based on the configuration.
81    /// Most users should use `builder()` instead.
82    #[must_use]
83    pub fn from_config(config: Config) -> Self {
84        // Create base HTTP client
85        let base_client = ReqwestClient::new();
86
87        // Build the HTTP client with retry and rate limiting
88        let http: Arc<dyn HttpClient> = if let Some(rate_limit_config) = config.rate_limit() {
89            // With rate limiting
90            let retry_policy = RetryPolicy::exponential(config.retry_attempts());
91            let retry_client = RetryClient::new(base_client, retry_policy);
92
93            let limiter = Arc::new(RateLimiter::new(
94                rate_limit_config.max_requests,
95                rate_limit_config.per_duration,
96            ));
97            Arc::new(RateLimitedClient::new(retry_client, limiter))
98        } else {
99            // Without rate limiting, just retry
100            let retry_policy = RetryPolicy::exponential(config.retry_attempts());
101            Arc::new(RetryClient::new(base_client, retry_policy))
102        };
103
104        let config = Arc::new(config);
105        let recent_tracks_client = RecentTracksClient::new(http.clone(), config.clone());
106        let loved_tracks_client = LovedTracksClient::new(http.clone(), config.clone());
107        let top_tracks_client = TopTracksClient::new(http, config.clone());
108
109        Self {
110            config,
111            recent_tracks_client,
112            loved_tracks_client,
113            top_tracks_client,
114        }
115    }
116
117    /// Create a new `LastFmClient` with a custom HTTP client
118    ///
119    /// This is primarily useful for testing with a mock HTTP client.
120    ///
121    /// # Example
122    /// ```
123    /// use lastfm_client::{LastFmClient, Config, ConfigBuilder};
124    /// use lastfm_client::client::MockClient;
125    /// use std::sync::Arc;
126    ///
127    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
128    /// let config = ConfigBuilder::new()
129    ///     .api_key("test_key")
130    ///     .build()?;
131    ///
132    /// let mock = MockClient::new();
133    /// let client = LastFmClient::with_http(config, Arc::new(mock));
134    /// # Ok(())
135    /// # }
136    /// ```
137    pub fn with_http(config: Config, http: Arc<dyn HttpClient>) -> Self {
138        let config = Arc::new(config);
139        let recent_tracks_client = RecentTracksClient::new(http.clone(), config.clone());
140        let loved_tracks_client = LovedTracksClient::new(http.clone(), config.clone());
141        let top_tracks_client = TopTracksClient::new(http, config.clone());
142
143        Self {
144            config,
145            recent_tracks_client,
146            loved_tracks_client,
147            top_tracks_client,
148        }
149    }
150
151    /// Get a builder for recent tracks requests
152    ///
153    /// # Example
154    /// ```no_run
155    /// # use lastfm_client::LastFmClient;
156    /// # async fn example(client: LastFmClient) -> Result<(), Box<dyn std::error::Error>> {
157    /// let tracks = client
158    ///     .recent_tracks("username")
159    ///     .limit(100)
160    ///     .fetch()
161    ///     .await?;
162    /// # Ok(())
163    /// # }
164    /// ```
165    pub fn recent_tracks(
166        &self,
167        username: impl Into<String>,
168    ) -> crate::api::RecentTracksRequestBuilder {
169        self.recent_tracks_client.builder(username)
170    }
171
172    /// Get a builder for loved tracks requests
173    ///
174    /// # Example
175    /// ```no_run
176    /// # use lastfm_client::LastFmClient;
177    /// # async fn example(client: LastFmClient) -> Result<(), Box<dyn std::error::Error>> {
178    /// let tracks = client
179    ///     .loved_tracks("username")
180    ///     .limit(100)
181    ///     .fetch()
182    ///     .await?;
183    /// # Ok(())
184    /// # }
185    /// ```
186    pub fn loved_tracks(
187        &self,
188        username: impl Into<String>,
189    ) -> crate::api::LovedTracksRequestBuilder {
190        self.loved_tracks_client.builder(username)
191    }
192
193    /// Get a builder for top tracks requests
194    ///
195    /// # Example
196    /// ```no_run
197    /// # use lastfm_client::LastFmClient;
198    /// # async fn example(client: LastFmClient) -> Result<(), Box<dyn std::error::Error>> {
199    /// let tracks = client
200    ///     .top_tracks("username")
201    ///     .limit(100)
202    ///     .fetch()
203    ///     .await?;
204    /// # Ok(())
205    /// # }
206    /// ```
207    pub fn top_tracks(&self, username: impl Into<String>) -> crate::api::TopTracksRequestBuilder {
208        self.top_tracks_client.builder(username)
209    }
210
211    /// Get a reference to the configuration
212    #[must_use]
213    pub fn config(&self) -> &Config {
214        &self.config
215    }
216}
217
218// Convenience: allow building the client directly from the ConfigBuilder
219impl ConfigBuilder {
220    /// Build a `LastFmClient` directly from this builder
221    ///
222    /// This is equivalent to calling `build().map(LastFmClient::from_config)`.
223    ///
224    /// # Errors
225    /// Returns an error if the builder is missing required fields (e.g., API key).
226    pub fn build_client(self) -> Result<LastFmClient> {
227        self.build().map(LastFmClient::from_config)
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use crate::client::MockClient;
235
236    #[test]
237    fn test_client_from_config() {
238        let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
239
240        let client = LastFmClient::from_config(config);
241        assert_eq!(client.config().api_key(), "test_key");
242    }
243
244    #[test]
245    fn test_client_with_mock() {
246        let config = ConfigBuilder::new().api_key("test_key").build().unwrap();
247
248        let mock = MockClient::new();
249        let client = LastFmClient::with_http(config, Arc::new(mock));
250        assert_eq!(client.config().api_key(), "test_key");
251    }
252
253    #[test]
254    fn test_builder() {
255        let client = LastFmClient::builder()
256            .api_key("test_key")
257            .build()
258            .map(LastFmClient::from_config)
259            .unwrap();
260
261        assert_eq!(client.config().api_key(), "test_key");
262    }
263}