1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Image {
8 pub url: String,
10 pub width: Option<u32>,
12 pub height: Option<u32>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ExternalUrls {
19 pub spotify: Option<String>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ExternalIds {
26 pub isrc: Option<String>,
28 pub ean: Option<String>,
30 pub upc: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Followers {
37 pub href: Option<String>,
39 pub total: u32,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Copyright {
46 pub text: String,
48 #[serde(rename = "type")]
50 pub copyright_type: String,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct Restrictions {
56 pub reason: String,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Paginated<T> {
63 pub href: String,
65 pub limit: u32,
67 pub next: Option<String>,
69 pub offset: u32,
71 pub previous: Option<String>,
73 pub total: u32,
75 pub items: Vec<T>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Cursored<T> {
82 pub href: String,
84 pub limit: u32,
86 pub next: Option<String>,
88 pub cursors: Option<Cursors>,
90 pub total: Option<u32>,
92 pub items: Vec<T>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct Cursors {
99 pub after: Option<String>,
101 pub before: Option<String>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ResumePoint {
108 pub fully_played: bool,
110 pub resume_position_ms: u64,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct LinkedFrom {
117 pub external_urls: Option<ExternalUrls>,
119 pub href: Option<String>,
121 pub id: Option<String>,
123 #[serde(rename = "type")]
125 pub item_type: Option<String>,
126 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}