spotify_cli/io/registry/
lists.rs1use serde_json::Value;
4
5use super::PayloadFormatter;
6use crate::io::formatters;
7use crate::io::output::PayloadKind;
8
9pub struct PlaylistsFormatter;
10
11impl PayloadFormatter for PlaylistsFormatter {
12 fn name(&self) -> &'static str {
13 "playlists"
14 }
15
16 fn supported_kinds(&self) -> &'static [PayloadKind] {
17 &[PayloadKind::PlaylistList]
18 }
19
20 fn matches(&self, payload: &Value) -> bool {
21 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
22 !items.is_empty()
23 && (items[0].get("tracks").is_some() || items[0].get("owner").is_some())
24 } else {
25 false
26 }
27 }
28
29 fn format(&self, payload: &Value, _message: &str) {
30 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
31 formatters::format_playlists(items);
32 }
33 }
34}
35
36pub struct SavedTracksFormatter;
37
38impl PayloadFormatter for SavedTracksFormatter {
39 fn name(&self) -> &'static str {
40 "saved_tracks"
41 }
42
43 fn supported_kinds(&self) -> &'static [PayloadKind] {
44 &[PayloadKind::SavedTracks]
45 }
46
47 fn matches(&self, payload: &Value) -> bool {
48 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
49 !items.is_empty()
50 && items[0].get("track").is_some()
51 && items[0].get("added_at").is_some()
52 } else {
53 false
54 }
55 }
56
57 fn format(&self, payload: &Value, _message: &str) {
58 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
59 formatters::format_saved_tracks(items);
60 }
61 }
62}
63
64pub struct TopTracksFormatter;
65
66impl PayloadFormatter for TopTracksFormatter {
67 fn name(&self) -> &'static str {
68 "top_tracks"
69 }
70
71 fn supported_kinds(&self) -> &'static [PayloadKind] {
72 &[PayloadKind::TopTracks, PayloadKind::TrackList]
73 }
74
75 fn matches(&self, payload: &Value) -> bool {
76 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
77 !items.is_empty() && items[0].get("album").is_some()
78 } else {
79 false
80 }
81 }
82
83 fn format(&self, payload: &Value, message: &str) {
84 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
85 formatters::format_top_tracks(items, message);
86 }
87 }
88}
89
90pub struct TopArtistsFormatter;
91
92impl PayloadFormatter for TopArtistsFormatter {
93 fn name(&self) -> &'static str {
94 "top_artists"
95 }
96
97 fn supported_kinds(&self) -> &'static [PayloadKind] {
98 &[
99 PayloadKind::TopArtists,
100 PayloadKind::ArtistList,
101 PayloadKind::FollowedArtists,
102 ]
103 }
104
105 fn matches(&self, payload: &Value) -> bool {
106 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
107 !items.is_empty() && items[0].get("genres").is_some()
108 } else {
109 false
110 }
111 }
112
113 fn format(&self, payload: &Value, message: &str) {
114 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
115 formatters::format_top_artists(items, message);
116 }
117 }
118}
119
120pub struct ArtistTopTracksFormatter;
121
122impl PayloadFormatter for ArtistTopTracksFormatter {
123 fn name(&self) -> &'static str {
124 "artist_top_tracks"
125 }
126
127 fn supported_kinds(&self) -> &'static [PayloadKind] {
128 &[PayloadKind::ArtistTopTracks]
129 }
130
131 fn matches(&self, payload: &Value) -> bool {
132 payload.get("tracks").map(|t| t.is_array()).unwrap_or(false)
133 && payload.get("items").is_none()
134 }
135
136 fn format(&self, payload: &Value, _message: &str) {
137 if let Some(tracks) = payload.get("tracks").and_then(|t| t.as_array()) {
138 formatters::format_artist_top_tracks(tracks);
139 }
140 }
141}
142
143pub struct LibraryCheckFormatter;
144
145impl PayloadFormatter for LibraryCheckFormatter {
146 fn name(&self) -> &'static str {
147 "library_check"
148 }
149
150 fn supported_kinds(&self) -> &'static [PayloadKind] {
151 &[PayloadKind::LibraryCheck]
152 }
153
154 fn matches(&self, payload: &Value) -> bool {
155 if let Some(arr) = payload.as_array() {
156 !arr.is_empty() && arr[0].is_boolean()
157 } else {
158 false
159 }
160 }
161
162 fn format(&self, payload: &Value, _message: &str) {
163 if let Some(arr) = payload.as_array() {
164 formatters::format_library_check(arr);
165 }
166 }
167}
168
169pub struct SavedAlbumsFormatter;
170
171impl PayloadFormatter for SavedAlbumsFormatter {
172 fn name(&self) -> &'static str {
173 "saved_albums"
174 }
175
176 fn supported_kinds(&self) -> &'static [PayloadKind] {
177 &[PayloadKind::SavedAlbums]
178 }
179
180 fn matches(&self, payload: &Value) -> bool {
181 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
183 !items.is_empty()
184 && items[0].get("album").is_some()
185 && items[0].get("added_at").is_some()
186 } else {
187 false
188 }
189 }
190
191 fn format(&self, payload: &Value, _message: &str) {
192 if let Some(items) = payload.get("items").and_then(|i| i.as_array()) {
193 formatters::format_saved_albums(items);
194 }
195 }
196}
197
198pub struct MarketsFormatter;
199
200impl PayloadFormatter for MarketsFormatter {
201 fn name(&self) -> &'static str {
202 "markets"
203 }
204
205 fn supported_kinds(&self) -> &'static [PayloadKind] {
206 &[PayloadKind::Markets]
207 }
208
209 fn matches(&self, payload: &Value) -> bool {
210 payload
211 .get("markets")
212 .map(|m| m.is_array())
213 .unwrap_or(false)
214 }
215
216 fn format(&self, payload: &Value, _message: &str) {
217 if let Some(markets) = payload.get("markets").and_then(|m| m.as_array()) {
218 formatters::format_markets(markets);
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use serde_json::json;
227
228 #[test]
229 fn playlist_formatter_supports_playlist_list() {
230 let formatter = PlaylistsFormatter;
231 let kinds = formatter.supported_kinds();
232 assert!(kinds.contains(&PayloadKind::PlaylistList));
233 }
234
235 #[test]
236 fn playlists_formatter_matches() {
237 let formatter = PlaylistsFormatter;
238 let payload = json!({ "items": [{ "tracks": {} }] });
239 assert!(formatter.matches(&payload));
240 let empty = json!({ "items": [] });
241 assert!(!formatter.matches(&empty));
242 }
243
244 #[test]
245 fn saved_tracks_formatter_matches() {
246 let formatter = SavedTracksFormatter;
247 let payload = json!({ "items": [{ "track": {}, "added_at": "2024" }] });
248 assert!(formatter.matches(&payload));
249 let empty = json!({ "items": [] });
250 assert!(!formatter.matches(&empty));
251 }
252
253 #[test]
254 fn top_tracks_formatter_matches() {
255 let formatter = TopTracksFormatter;
256 let payload = json!({ "items": [{ "album": {} }] });
257 assert!(formatter.matches(&payload));
258 let empty = json!({ "items": [] });
259 assert!(!formatter.matches(&empty));
260 }
261
262 #[test]
263 fn top_artists_formatter_supports_multiple_kinds() {
264 let formatter = TopArtistsFormatter;
265 let kinds = formatter.supported_kinds();
266 assert!(kinds.contains(&PayloadKind::TopArtists));
267 assert!(kinds.contains(&PayloadKind::ArtistList));
268 assert!(kinds.contains(&PayloadKind::FollowedArtists));
269 }
270
271 #[test]
272 fn top_artists_formatter_matches() {
273 let formatter = TopArtistsFormatter;
274 let payload = json!({ "items": [{ "genres": [] }] });
275 assert!(formatter.matches(&payload));
276 let empty = json!({ "items": [] });
277 assert!(!formatter.matches(&empty));
278 }
279
280 #[test]
281 fn artist_top_tracks_formatter_matches() {
282 let formatter = ArtistTopTracksFormatter;
283 let payload = json!({ "tracks": [] });
284 assert!(formatter.matches(&payload));
285 let with_items = json!({ "tracks": [], "items": [] });
286 assert!(!formatter.matches(&with_items));
287 }
288
289 #[test]
290 fn library_check_matches_boolean_array() {
291 let formatter = LibraryCheckFormatter;
292 let payload = json!([true, false, true]);
293 assert!(formatter.matches(&payload));
294 }
295
296 #[test]
297 fn library_check_does_not_match_non_boolean_array() {
298 let formatter = LibraryCheckFormatter;
299 let payload = json!(["string", "array"]);
300 assert!(!formatter.matches(&payload));
301 }
302
303 #[test]
304 fn formatter_names() {
305 assert_eq!(PlaylistsFormatter.name(), "playlists");
306 assert_eq!(SavedTracksFormatter.name(), "saved_tracks");
307 assert_eq!(TopTracksFormatter.name(), "top_tracks");
308 assert_eq!(TopArtistsFormatter.name(), "top_artists");
309 assert_eq!(ArtistTopTracksFormatter.name(), "artist_top_tracks");
310 assert_eq!(LibraryCheckFormatter.name(), "library_check");
311 }
312
313 #[test]
314 fn playlists_format_runs() {
315 let formatter = PlaylistsFormatter;
316 let payload = json!({
317 "items": [{
318 "name": "My Playlist",
319 "tracks": {"total": 10},
320 "owner": {"display_name": "User"}
321 }]
322 });
323 formatter.format(&payload, "Playlists");
324 }
325
326 #[test]
327 fn saved_tracks_format_runs() {
328 let formatter = SavedTracksFormatter;
329 let payload = json!({
330 "items": [{
331 "track": {"name": "Track", "artists": [{"name": "Artist"}]},
332 "added_at": "2024-01-01"
333 }]
334 });
335 formatter.format(&payload, "Saved Tracks");
336 }
337
338 #[test]
339 fn top_tracks_format_runs() {
340 let formatter = TopTracksFormatter;
341 let payload = json!({
342 "items": [{
343 "name": "Track",
344 "album": {"name": "Album"},
345 "artists": [{"name": "Artist"}]
346 }]
347 });
348 formatter.format(&payload, "Top Tracks");
349 }
350
351 #[test]
352 fn top_artists_format_runs() {
353 let formatter = TopArtistsFormatter;
354 let payload = json!({
355 "items": [{
356 "name": "Artist",
357 "genres": ["rock"],
358 "popularity": 80
359 }]
360 });
361 formatter.format(&payload, "Top Artists");
362 }
363
364 #[test]
365 fn artist_top_tracks_format_runs() {
366 let formatter = ArtistTopTracksFormatter;
367 let payload = json!({
368 "tracks": [{
369 "name": "Track",
370 "album": {"name": "Album"}
371 }]
372 });
373 formatter.format(&payload, "Artist Top Tracks");
374 }
375
376 #[test]
377 fn library_check_format_runs() {
378 let formatter = LibraryCheckFormatter;
379 let payload = json!([true, false, true]);
380 formatter.format(&payload, "Library Check");
381 }
382
383 #[test]
384 fn playlists_matches_with_owner() {
385 let formatter = PlaylistsFormatter;
386 let payload = json!({ "items": [{ "owner": {} }] });
387 assert!(formatter.matches(&payload));
388 }
389
390 #[test]
391 fn saved_tracks_does_not_match_without_added_at() {
392 let formatter = SavedTracksFormatter;
393 let payload = json!({ "items": [{ "track": {} }] });
394 assert!(!formatter.matches(&payload));
395 }
396
397 #[test]
398 fn library_check_does_not_match_empty_array() {
399 let formatter = LibraryCheckFormatter;
400 let payload = json!([]);
401 assert!(!formatter.matches(&payload));
402 }
403
404 #[test]
405 fn library_check_does_not_match_non_array() {
406 let formatter = LibraryCheckFormatter;
407 let payload = json!({ "data": true });
408 assert!(!formatter.matches(&payload));
409 }
410}