lastfm_client/api/user/
friends.rs1use 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#[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 #[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 #[must_use]
44 pub const fn page(mut self, page: u32) -> Self {
45 self.page = Some(page);
46
47 self
48 }
49
50 #[must_use]
52 pub const fn with_recent_tracks(mut self) -> Self {
53 self.recent_tracks = true;
54
55 self
56 }
57
58 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 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}