spotify_cli/types/
common.rs

1//! Common types shared across multiple Spotify API responses.
2
3use serde::{Deserialize, Serialize};
4
5/// Image object returned by Spotify.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Image {
8    /// Image URL.
9    pub url: String,
10    /// Image width in pixels (may be null).
11    pub width: Option<u32>,
12    /// Image height in pixels (may be null).
13    pub height: Option<u32>,
14}
15
16/// External URLs for a Spotify object.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ExternalUrls {
19    /// Spotify URL for the object.
20    pub spotify: Option<String>,
21}
22
23/// External IDs (ISRC, EAN, UPC).
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ExternalIds {
26    /// International Standard Recording Code.
27    pub isrc: Option<String>,
28    /// International Article Number.
29    pub ean: Option<String>,
30    /// Universal Product Code.
31    pub upc: Option<String>,
32}
33
34/// Follower information.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Followers {
37    /// Spotify URL for followers (always null per API docs).
38    pub href: Option<String>,
39    /// Total number of followers.
40    pub total: u32,
41}
42
43/// Copyright information.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Copyright {
46    /// Copyright text.
47    pub text: String,
48    /// Copyright type: C = copyright, P = performance copyright.
49    #[serde(rename = "type")]
50    pub copyright_type: String,
51}
52
53/// Restriction information.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct Restrictions {
56    /// Reason for restriction: market, product, explicit.
57    pub reason: String,
58}
59
60/// Paginated response wrapper.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Paginated<T> {
63    /// URL to the API endpoint for this page.
64    pub href: String,
65    /// Maximum number of items in the response.
66    pub limit: u32,
67    /// URL to the next page (null if last page).
68    pub next: Option<String>,
69    /// Offset of the items returned.
70    pub offset: u32,
71    /// URL to the previous page (null if first page).
72    pub previous: Option<String>,
73    /// Total number of items available.
74    pub total: u32,
75    /// The requested items.
76    pub items: Vec<T>,
77}
78
79/// Cursored response wrapper (for endpoints using cursor pagination).
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Cursored<T> {
82    /// URL to the API endpoint.
83    pub href: String,
84    /// Maximum number of items.
85    pub limit: u32,
86    /// URL to the next page.
87    pub next: Option<String>,
88    /// Cursors for pagination.
89    pub cursors: Option<Cursors>,
90    /// Total number of items (may be null).
91    pub total: Option<u32>,
92    /// The requested items.
93    pub items: Vec<T>,
94}
95
96/// Cursor positions for cursor-based pagination.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct Cursors {
99    /// Cursor to the next page.
100    pub after: Option<String>,
101    /// Cursor to the previous page.
102    pub before: Option<String>,
103}
104
105/// Resume point for podcasts/audiobooks.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ResumePoint {
108    /// Whether playback has been fully completed.
109    pub fully_played: bool,
110    /// Position in milliseconds where playback was paused.
111    pub resume_position_ms: u64,
112}
113
114/// Linked track (relinked due to regional restrictions).
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct LinkedFrom {
117    /// External URLs.
118    pub external_urls: Option<ExternalUrls>,
119    /// Spotify URL.
120    pub href: Option<String>,
121    /// Spotify ID.
122    pub id: Option<String>,
123    /// Object type.
124    #[serde(rename = "type")]
125    pub item_type: Option<String>,
126    /// Spotify URI.
127    pub uri: Option<String>,
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use serde_json::json;
134
135    #[test]
136    fn image_deserializes() {
137        let json = json!({
138            "url": "https://image.jpg",
139            "width": 640,
140            "height": 480
141        });
142        let image: Image = serde_json::from_value(json).unwrap();
143        assert_eq!(image.url, "https://image.jpg");
144        assert_eq!(image.width, Some(640));
145        assert_eq!(image.height, Some(480));
146    }
147
148    #[test]
149    fn image_deserializes_with_null_dimensions() {
150        let json = json!({
151            "url": "https://image.jpg"
152        });
153        let image: Image = serde_json::from_value(json).unwrap();
154        assert!(image.width.is_none());
155        assert!(image.height.is_none());
156    }
157
158    #[test]
159    fn external_urls_deserializes() {
160        let json = json!({
161            "spotify": "https://open.spotify.com/track/123"
162        });
163        let urls: ExternalUrls = serde_json::from_value(json).unwrap();
164        assert_eq!(
165            urls.spotify,
166            Some("https://open.spotify.com/track/123".to_string())
167        );
168    }
169
170    #[test]
171    fn external_ids_deserializes() {
172        let json = json!({
173            "isrc": "USRC12345678",
174            "ean": "1234567890123",
175            "upc": "012345678905"
176        });
177        let ids: ExternalIds = serde_json::from_value(json).unwrap();
178        assert_eq!(ids.isrc, Some("USRC12345678".to_string()));
179        assert_eq!(ids.ean, Some("1234567890123".to_string()));
180        assert_eq!(ids.upc, Some("012345678905".to_string()));
181    }
182
183    #[test]
184    fn followers_deserializes() {
185        let json = json!({
186            "total": 1000000
187        });
188        let followers: Followers = serde_json::from_value(json).unwrap();
189        assert_eq!(followers.total, 1000000);
190        assert!(followers.href.is_none());
191    }
192
193    #[test]
194    fn copyright_deserializes() {
195        let json = json!({
196            "text": "(C) 2024 Test Records",
197            "type": "C"
198        });
199        let copyright: Copyright = serde_json::from_value(json).unwrap();
200        assert_eq!(copyright.text, "(C) 2024 Test Records");
201        assert_eq!(copyright.copyright_type, "C");
202    }
203
204    #[test]
205    fn restrictions_deserializes() {
206        let json = json!({
207            "reason": "market"
208        });
209        let restrictions: Restrictions = serde_json::from_value(json).unwrap();
210        assert_eq!(restrictions.reason, "market");
211    }
212
213    #[test]
214    fn paginated_deserializes() {
215        let json = json!({
216            "href": "https://api.spotify.com/v1/me/tracks",
217            "limit": 20,
218            "offset": 0,
219            "total": 100,
220            "items": [1, 2, 3]
221        });
222        let paginated: Paginated<i32> = serde_json::from_value(json).unwrap();
223        assert_eq!(paginated.limit, 20);
224        assert_eq!(paginated.total, 100);
225        assert_eq!(paginated.items, vec![1, 2, 3]);
226    }
227
228    #[test]
229    fn paginated_with_next_prev() {
230        let json = json!({
231            "href": "https://api.spotify.com/v1/me/tracks?offset=20",
232            "limit": 20,
233            "offset": 20,
234            "total": 100,
235            "next": "https://api.spotify.com/v1/me/tracks?offset=40",
236            "previous": "https://api.spotify.com/v1/me/tracks?offset=0",
237            "items": []
238        });
239        let paginated: Paginated<i32> = serde_json::from_value(json).unwrap();
240        assert!(paginated.next.is_some());
241        assert!(paginated.previous.is_some());
242    }
243
244    #[test]
245    fn cursored_deserializes() {
246        let json = json!({
247            "href": "https://api.spotify.com/v1/me/following",
248            "limit": 20,
249            "total": 50,
250            "items": ["a", "b"],
251            "cursors": {"after": "cursor123", "before": "cursor000"}
252        });
253        let cursored: Cursored<String> = serde_json::from_value(json).unwrap();
254        assert_eq!(cursored.limit, 20);
255        assert_eq!(cursored.items.len(), 2);
256        assert!(cursored.cursors.is_some());
257    }
258
259    #[test]
260    fn cursors_deserializes() {
261        let json = json!({
262            "after": "next_cursor",
263            "before": "prev_cursor"
264        });
265        let cursors: Cursors = serde_json::from_value(json).unwrap();
266        assert_eq!(cursors.after, Some("next_cursor".to_string()));
267        assert_eq!(cursors.before, Some("prev_cursor".to_string()));
268    }
269
270    #[test]
271    fn resume_point_deserializes() {
272        let json = json!({
273            "fully_played": false,
274            "resume_position_ms": 120000
275        });
276        let resume: ResumePoint = serde_json::from_value(json).unwrap();
277        assert!(!resume.fully_played);
278        assert_eq!(resume.resume_position_ms, 120000);
279    }
280
281    #[test]
282    fn linked_from_deserializes() {
283        let json = json!({
284            "id": "track123",
285            "type": "track",
286            "uri": "spotify:track:track123",
287            "href": "https://api.spotify.com/v1/tracks/track123"
288        });
289        let linked: LinkedFrom = serde_json::from_value(json).unwrap();
290        assert_eq!(linked.id, Some("track123".to_string()));
291        assert_eq!(linked.item_type, Some("track".to_string()));
292    }
293}