spotify_cli/types/
user.rs

1//! User types from Spotify API.
2
3use serde::{Deserialize, Serialize};
4
5use super::common::{ExternalUrls, Followers, Image};
6
7/// Public user profile (limited information).
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct UserPublic {
10    /// Display name.
11    pub display_name: Option<String>,
12    /// External URLs.
13    pub external_urls: Option<ExternalUrls>,
14    /// Follower information.
15    pub followers: Option<Followers>,
16    /// Spotify URL.
17    pub href: Option<String>,
18    /// Spotify user ID.
19    pub id: String,
20    /// User profile images.
21    pub images: Option<Vec<Image>>,
22    /// Object type (always "user").
23    #[serde(rename = "type")]
24    pub item_type: String,
25    /// Spotify URI.
26    pub uri: String,
27}
28
29/// Private user profile (current user with full details).
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct UserPrivate {
32    /// Country (ISO 3166-1 alpha-2).
33    pub country: Option<String>,
34    /// Display name.
35    pub display_name: Option<String>,
36    /// Email address.
37    pub email: Option<String>,
38    /// Explicit content settings.
39    pub explicit_content: Option<ExplicitContent>,
40    /// External URLs.
41    pub external_urls: Option<ExternalUrls>,
42    /// Follower information.
43    pub followers: Option<Followers>,
44    /// Spotify URL.
45    pub href: Option<String>,
46    /// Spotify user ID.
47    pub id: String,
48    /// User profile images.
49    pub images: Option<Vec<Image>>,
50    /// Product type (premium, free, etc.).
51    pub product: Option<String>,
52    /// Object type.
53    #[serde(rename = "type")]
54    pub item_type: String,
55    /// Spotify URI.
56    pub uri: String,
57}
58
59/// Explicit content filter settings.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ExplicitContent {
62    /// Whether to filter explicit content.
63    pub filter_enabled: Option<bool>,
64    /// Whether filter is locked (can't be changed).
65    pub filter_locked: Option<bool>,
66}
67
68impl UserPrivate {
69    /// Check if user has premium subscription.
70    pub fn is_premium(&self) -> bool {
71        self.product.as_deref() == Some("premium")
72    }
73
74    /// Get the largest image URL if available.
75    pub fn image_url(&self) -> Option<&str> {
76        self.images
77            .as_ref()
78            .and_then(|imgs| imgs.first())
79            .map(|img| img.url.as_str())
80    }
81}
82
83impl UserPublic {
84    /// Get the largest image URL if available.
85    pub fn image_url(&self) -> Option<&str> {
86        self.images
87            .as_ref()
88            .and_then(|imgs| imgs.first())
89            .map(|img| img.url.as_str())
90    }
91}
92
93/// Top items response (tracks or artists).
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct TopTracksResponse {
96    /// URL to the API endpoint.
97    pub href: Option<String>,
98    /// Maximum number of items.
99    pub limit: Option<u32>,
100    /// URL to the next page.
101    pub next: Option<String>,
102    /// Offset of items returned.
103    pub offset: Option<u32>,
104    /// URL to the previous page.
105    pub previous: Option<String>,
106    /// Total number of items.
107    pub total: Option<u32>,
108    /// The top tracks.
109    pub items: Vec<super::track::Track>,
110}
111
112/// Top artists response.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct TopArtistsResponse {
115    /// URL to the API endpoint.
116    pub href: Option<String>,
117    /// Maximum number of items.
118    pub limit: Option<u32>,
119    /// URL to the next page.
120    pub next: Option<String>,
121    /// Offset of items returned.
122    pub offset: Option<u32>,
123    /// URL to the previous page.
124    pub previous: Option<String>,
125    /// Total number of items.
126    pub total: Option<u32>,
127    /// The top artists.
128    pub items: Vec<super::artist::Artist>,
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use serde_json::json;
135
136    #[test]
137    fn user_public_deserializes() {
138        let json = json!({
139            "id": "user123",
140            "type": "user",
141            "uri": "spotify:user:user123",
142            "display_name": "John Doe"
143        });
144        let user: UserPublic = serde_json::from_value(json).unwrap();
145        assert_eq!(user.id, "user123");
146        assert_eq!(user.display_name, Some("John Doe".to_string()));
147    }
148
149    #[test]
150    fn user_public_image_url() {
151        let json = json!({
152            "id": "user123",
153            "type": "user",
154            "uri": "spotify:user:user123",
155            "images": [{"url": "https://profile.jpg", "height": 300, "width": 300}]
156        });
157        let user: UserPublic = serde_json::from_value(json).unwrap();
158        assert_eq!(user.image_url(), Some("https://profile.jpg"));
159    }
160
161    #[test]
162    fn user_public_image_url_none_when_empty() {
163        let json = json!({
164            "id": "user123",
165            "type": "user",
166            "uri": "spotify:user:user123"
167        });
168        let user: UserPublic = serde_json::from_value(json).unwrap();
169        assert!(user.image_url().is_none());
170    }
171
172    #[test]
173    fn user_private_deserializes() {
174        let json = json!({
175            "id": "user123",
176            "type": "user",
177            "uri": "spotify:user:user123",
178            "email": "user@example.com",
179            "product": "premium",
180            "country": "US"
181        });
182        let user: UserPrivate = serde_json::from_value(json).unwrap();
183        assert_eq!(user.id, "user123");
184        assert_eq!(user.email, Some("user@example.com".to_string()));
185        assert!(user.is_premium());
186    }
187
188    #[test]
189    fn user_private_is_premium_false_for_free() {
190        let json = json!({
191            "id": "user123",
192            "type": "user",
193            "uri": "spotify:user:user123",
194            "product": "free"
195        });
196        let user: UserPrivate = serde_json::from_value(json).unwrap();
197        assert!(!user.is_premium());
198    }
199
200    #[test]
201    fn user_private_is_premium_false_when_none() {
202        let json = json!({
203            "id": "user123",
204            "type": "user",
205            "uri": "spotify:user:user123"
206        });
207        let user: UserPrivate = serde_json::from_value(json).unwrap();
208        assert!(!user.is_premium());
209    }
210
211    #[test]
212    fn explicit_content_deserializes() {
213        let json = json!({
214            "filter_enabled": true,
215            "filter_locked": false
216        });
217        let explicit: ExplicitContent = serde_json::from_value(json).unwrap();
218        assert_eq!(explicit.filter_enabled, Some(true));
219        assert_eq!(explicit.filter_locked, Some(false));
220    }
221
222    #[test]
223    fn top_tracks_response_deserializes() {
224        let json = json!({
225            "items": [],
226            "total": 50,
227            "limit": 20,
228            "offset": 0
229        });
230        let resp: TopTracksResponse = serde_json::from_value(json).unwrap();
231        assert!(resp.items.is_empty());
232        assert_eq!(resp.total, Some(50));
233    }
234
235    #[test]
236    fn top_artists_response_deserializes() {
237        let json = json!({
238            "items": [],
239            "total": 50,
240            "limit": 20,
241            "offset": 0
242        });
243        let resp: TopArtistsResponse = serde_json::from_value(json).unwrap();
244        assert!(resp.items.is_empty());
245        assert_eq!(resp.total, Some(50));
246    }
247
248    #[test]
249    fn user_private_image_url_returns_first() {
250        let json = json!({
251            "id": "user123",
252            "type": "user",
253            "uri": "spotify:user:user123",
254            "images": [
255                {"url": "https://first.jpg", "height": 640, "width": 640},
256                {"url": "https://second.jpg", "height": 300, "width": 300}
257            ]
258        });
259        let user: UserPrivate = serde_json::from_value(json).unwrap();
260        assert_eq!(user.image_url(), Some("https://first.jpg"));
261    }
262
263    #[test]
264    fn user_private_image_url_none_when_empty() {
265        let json = json!({
266            "id": "user123",
267            "type": "user",
268            "uri": "spotify:user:user123"
269        });
270        let user: UserPrivate = serde_json::from_value(json).unwrap();
271        assert!(user.image_url().is_none());
272    }
273}