Skip to main content

lastfm_client/api/user/
friends.rs

1use std::sync::Arc;
2
3use crate::api::constants::{BASE_URL, METHOD_FRIENDS};
4use crate::api::user_params;
5use crate::client::HttpClient;
6use crate::config::Config;
7use crate::error::Result;
8use crate::types::{FriendProfile, FriendsPage, FriendsResponse};
9use crate::url_builder::Url;
10
11/// Builder for `user.getFriends` requests
12#[derive(Debug)]
13pub struct FriendsRequestBuilder {
14    http: Arc<dyn HttpClient>,
15    config: Arc<Config>,
16    username: String,
17    limit: Option<u32>,
18    page: Option<u32>,
19    recent_tracks: bool,
20}
21
22impl FriendsRequestBuilder {
23    pub(crate) fn new(http: Arc<dyn HttpClient>, config: Arc<Config>, username: String) -> Self {
24        Self {
25            http,
26            config,
27            username,
28            limit: None,
29            page: None,
30            recent_tracks: false,
31        }
32    }
33
34    /// Set the number of results per page (default 50, max 50).
35    #[must_use]
36    pub const fn limit(mut self, limit: u32) -> Self {
37        self.limit = Some(if limit < 50 { limit } else { 50 });
38
39        self
40    }
41
42    /// Set the page number (1-indexed).
43    #[must_use]
44    pub const fn page(mut self, page: u32) -> Self {
45        self.page = Some(page);
46
47        self
48    }
49
50    /// Include the user's most recent tracks in the response.
51    #[must_use]
52    pub const fn with_recent_tracks(mut self) -> Self {
53        self.recent_tracks = true;
54
55        self
56    }
57
58    /// Fetch one page of friends.
59    ///
60    /// # Errors
61    /// Returns an error if the HTTP request fails or the response cannot be parsed.
62    pub async fn fetch_page(self) -> Result<FriendsPage> {
63        let mut params = user_params(METHOD_FRIENDS, &self.username, self.config.api_key());
64
65        if let Some(limit) = self.limit {
66            params.insert("limit".to_string(), limit.to_string());
67        }
68
69        if let Some(page) = self.page {
70            params.insert("page".to_string(), page.to_string());
71        }
72
73        if self.recent_tracks {
74            params.insert("recenttracks".to_string(), "1".to_string());
75        }
76
77        let url = Url::new(BASE_URL).add_args(params).build();
78        let value = self.http.get(&url).await?;
79        let response: FriendsResponse = serde_json::from_value(value)?;
80
81        Ok(FriendsPage::from(response))
82    }
83
84    /// Fetch all friends across all pages.
85    ///
86    /// # Errors
87    /// Returns an error if any HTTP request fails or any response cannot be parsed.
88    pub async fn fetch_all(self) -> Result<Vec<FriendProfile>> {
89        let mut all_friends = Vec::new();
90        let mut page = 1u32;
91
92        loop {
93            let mut params = user_params(METHOD_FRIENDS, &self.username, self.config.api_key());
94            params.insert("page".to_string(), page.to_string());
95
96            if let Some(l) = self.limit {
97                params.insert("limit".to_string(), l.to_string());
98            }
99
100            if self.recent_tracks {
101                params.insert("recenttracks".to_string(), "1".to_string());
102            }
103
104            let url = Url::new(BASE_URL).add_args(params).build();
105            let value = self.http.get(&url).await?;
106            let response: FriendsResponse = serde_json::from_value(value)?;
107            let friends_page = FriendsPage::from(response);
108
109            let total_pages = friends_page.total_pages;
110            all_friends.extend(friends_page.friends);
111
112            if page >= total_pages {
113                break;
114            }
115
116            page += 1;
117        }
118
119        Ok(all_friends)
120    }
121}
122
123#[cfg(test)]
124#[allow(clippy::unwrap_used)]
125mod tests {
126    use super::*;
127    use crate::client::MockClient;
128    use crate::config::ConfigBuilder;
129    use serde_json::json;
130    use std::sync::Arc;
131
132    fn make_builder(response: serde_json::Value) -> FriendsRequestBuilder {
133        let config = Arc::new(ConfigBuilder::new().api_key("test_key").build().unwrap());
134        let mock = Arc::new(MockClient::new().with_response("user.getfriends", response));
135
136        FriendsRequestBuilder::new(mock, config, "testuser".to_string())
137    }
138
139    fn friend_json(name: &str) -> serde_json::Value {
140        json!({
141            "name": name,
142            "realname": "",
143            "url": format!("https://www.last.fm/user/{name}"),
144            "country": "UK",
145            "subscriber": "0",
146            "image": [],
147            "registered": { "unixtime": "1108296000", "#text": "2005-02-13 00:00" }
148        })
149    }
150
151    #[tokio::test]
152    async fn test_fetch_page() {
153        let builder = make_builder(json!({
154            "friends": {
155                "@attr": { "user": "testuser", "total": "2", "page": "1", "totalPages": "1", "perPage": "50" },
156                "user": [friend_json("alice"), friend_json("bob")]
157            }
158        }));
159
160        let page = builder.fetch_page().await.unwrap();
161        assert_eq!(page.friends.len(), 2);
162        assert_eq!(page.total, 2);
163        assert_eq!(page.friends[0].name, "alice");
164        assert_eq!(page.friends[1].name, "bob");
165    }
166
167    #[tokio::test]
168    async fn test_fetch_all_single_page() {
169        let builder = make_builder(json!({
170            "friends": {
171                "@attr": { "user": "testuser", "total": "1", "page": "1", "totalPages": "1", "perPage": "50" },
172                "user": [friend_json("alice")]
173            }
174        }));
175
176        let friends = builder.fetch_all().await.unwrap();
177        assert_eq!(friends.len(), 1);
178        assert_eq!(friends[0].name, "alice");
179    }
180
181    #[test]
182    fn test_limit_clamped_to_50() {
183        let config = Arc::new(ConfigBuilder::new().api_key("test_key").build().unwrap());
184        let mock = Arc::new(MockClient::new());
185        let builder = FriendsRequestBuilder::new(mock, config, "testuser".to_string()).limit(100);
186        assert_eq!(builder.limit, Some(50));
187    }
188}