spotify_cli/io/registry/
resources.rs

1//! Individual resource detail formatters (track, album, artist, playlist, user, category)
2
3use serde_json::Value;
4
5use super::PayloadFormatter;
6use crate::io::formatters;
7use crate::io::output::PayloadKind;
8
9pub struct TrackDetailFormatter;
10
11impl PayloadFormatter for TrackDetailFormatter {
12    fn name(&self) -> &'static str {
13        "track_detail"
14    }
15
16    fn supported_kinds(&self) -> &'static [PayloadKind] {
17        &[PayloadKind::Track]
18    }
19
20    fn matches(&self, payload: &Value) -> bool {
21        payload.get("album").is_some()
22            && payload.get("artists").is_some()
23            && payload.get("duration_ms").is_some()
24    }
25
26    fn format(&self, payload: &Value, _message: &str) {
27        formatters::format_track_detail(payload);
28    }
29}
30
31pub struct AlbumDetailFormatter;
32
33impl PayloadFormatter for AlbumDetailFormatter {
34    fn name(&self) -> &'static str {
35        "album_detail"
36    }
37
38    fn supported_kinds(&self) -> &'static [PayloadKind] {
39        &[PayloadKind::Album]
40    }
41
42    fn matches(&self, payload: &Value) -> bool {
43        payload.get("album_type").is_some() && payload.get("tracks").is_some()
44    }
45
46    fn format(&self, payload: &Value, _message: &str) {
47        formatters::format_album_detail(payload);
48    }
49}
50
51pub struct ArtistDetailFormatter;
52
53impl PayloadFormatter for ArtistDetailFormatter {
54    fn name(&self) -> &'static str {
55        "artist_detail"
56    }
57
58    fn supported_kinds(&self) -> &'static [PayloadKind] {
59        &[PayloadKind::Artist, PayloadKind::RelatedArtists]
60    }
61
62    fn matches(&self, payload: &Value) -> bool {
63        payload.get("followers").is_some()
64            && payload.get("genres").is_some()
65            && payload.get("album").is_none()
66    }
67
68    fn format(&self, payload: &Value, _message: &str) {
69        formatters::format_artist_detail(payload);
70    }
71}
72
73pub struct PlaylistDetailFormatter;
74
75impl PayloadFormatter for PlaylistDetailFormatter {
76    fn name(&self) -> &'static str {
77        "playlist_detail"
78    }
79
80    fn supported_kinds(&self) -> &'static [PayloadKind] {
81        &[PayloadKind::Playlist]
82    }
83
84    fn matches(&self, payload: &Value) -> bool {
85        payload.get("owner").is_some() && payload.get("tracks").is_some()
86    }
87
88    fn format(&self, payload: &Value, _message: &str) {
89        formatters::format_playlist_detail(payload);
90    }
91}
92
93pub struct UserProfileFormatter;
94
95impl PayloadFormatter for UserProfileFormatter {
96    fn name(&self) -> &'static str {
97        "user_profile"
98    }
99
100    fn supported_kinds(&self) -> &'static [PayloadKind] {
101        &[PayloadKind::User]
102    }
103
104    fn matches(&self, payload: &Value) -> bool {
105        payload.get("display_name").is_some()
106            && payload.get("product").is_some()
107            && payload.get("genres").is_none()
108    }
109
110    fn format(&self, payload: &Value, _message: &str) {
111        formatters::format_user_profile(payload);
112    }
113}
114
115pub struct CategoryListFormatter;
116
117impl PayloadFormatter for CategoryListFormatter {
118    fn name(&self) -> &'static str {
119        "category_list"
120    }
121
122    fn supported_kinds(&self) -> &'static [PayloadKind] {
123        &[PayloadKind::CategoryList]
124    }
125
126    fn matches(&self, payload: &Value) -> bool {
127        payload
128            .get("categories")
129            .and_then(|c| c.get("items"))
130            .is_some()
131    }
132
133    fn format(&self, payload: &Value, _message: &str) {
134        if let Some(items) = payload
135            .get("categories")
136            .and_then(|c| c.get("items"))
137            .and_then(|i| i.as_array())
138        {
139            formatters::format_categories(items);
140        }
141    }
142}
143
144pub struct CategoryDetailFormatter;
145
146impl PayloadFormatter for CategoryDetailFormatter {
147    fn name(&self) -> &'static str {
148        "category_detail"
149    }
150
151    fn supported_kinds(&self) -> &'static [PayloadKind] {
152        &[PayloadKind::Category]
153    }
154
155    fn matches(&self, payload: &Value) -> bool {
156        payload.get("icons").is_some()
157            && payload.get("id").is_some()
158            && payload.get("followers").is_none()
159            && payload.get("owner").is_none()
160    }
161
162    fn format(&self, payload: &Value, _message: &str) {
163        formatters::format_category_detail(payload);
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use serde_json::json;
171
172    #[test]
173    fn track_formatter_supports_kind() {
174        let formatter = TrackDetailFormatter;
175        assert!(formatter.supported_kinds().contains(&PayloadKind::Track));
176    }
177
178    #[test]
179    fn track_detail_matches_payload() {
180        let formatter = TrackDetailFormatter;
181        let payload = json!({
182            "album": {"name": "Album"},
183            "artists": [{"name": "Artist"}],
184            "duration_ms": 300000
185        });
186        assert!(formatter.matches(&payload));
187    }
188
189    #[test]
190    fn album_detail_formatter_matches() {
191        let formatter = AlbumDetailFormatter;
192        let payload = json!({ "album_type": "album", "tracks": {} });
193        assert!(formatter.matches(&payload));
194        assert!(!formatter.matches(&json!({})));
195    }
196
197    #[test]
198    fn artist_detail_formatter_matches() {
199        let formatter = ArtistDetailFormatter;
200        let payload = json!({ "followers": {}, "genres": [] });
201        assert!(formatter.matches(&payload));
202        let with_album = json!({ "followers": {}, "genres": [], "album": {} });
203        assert!(!formatter.matches(&with_album));
204    }
205
206    #[test]
207    fn playlist_detail_formatter_matches() {
208        let formatter = PlaylistDetailFormatter;
209        let payload = json!({ "owner": {}, "tracks": {} });
210        assert!(formatter.matches(&payload));
211        assert!(!formatter.matches(&json!({})));
212    }
213
214    #[test]
215    fn user_profile_formatter_matches() {
216        let formatter = UserProfileFormatter;
217        let payload = json!({ "display_name": "User", "product": "premium" });
218        assert!(formatter.matches(&payload));
219        let with_genres = json!({ "display_name": "User", "product": "premium", "genres": [] });
220        assert!(!formatter.matches(&with_genres));
221    }
222
223    #[test]
224    fn category_list_formatter_matches() {
225        let formatter = CategoryListFormatter;
226        let payload = json!({ "categories": { "items": [] } });
227        assert!(formatter.matches(&payload));
228        assert!(!formatter.matches(&json!({})));
229    }
230
231    #[test]
232    fn category_detail_formatter_matches() {
233        let formatter = CategoryDetailFormatter;
234        let payload = json!({ "icons": [], "id": "test" });
235        assert!(formatter.matches(&payload));
236        let with_followers = json!({ "icons": [], "id": "test", "followers": {} });
237        assert!(!formatter.matches(&with_followers));
238    }
239
240    #[test]
241    fn formatter_names() {
242        assert_eq!(TrackDetailFormatter.name(), "track_detail");
243        assert_eq!(AlbumDetailFormatter.name(), "album_detail");
244        assert_eq!(ArtistDetailFormatter.name(), "artist_detail");
245        assert_eq!(PlaylistDetailFormatter.name(), "playlist_detail");
246        assert_eq!(UserProfileFormatter.name(), "user_profile");
247        assert_eq!(CategoryListFormatter.name(), "category_list");
248        assert_eq!(CategoryDetailFormatter.name(), "category_detail");
249    }
250
251    #[test]
252    fn track_detail_format_runs() {
253        let formatter = TrackDetailFormatter;
254        let payload = json!({
255            "name": "Test Track",
256            "album": {"name": "Album"},
257            "artists": [{"name": "Artist"}],
258            "duration_ms": 180000,
259            "popularity": 80
260        });
261        formatter.format(&payload, "Track");
262    }
263
264    #[test]
265    fn album_detail_format_runs() {
266        let formatter = AlbumDetailFormatter;
267        let payload = json!({
268            "name": "Test Album",
269            "album_type": "album",
270            "tracks": {"items": []},
271            "artists": [{"name": "Artist"}],
272            "release_date": "2024"
273        });
274        formatter.format(&payload, "Album");
275    }
276
277    #[test]
278    fn artist_detail_format_runs() {
279        let formatter = ArtistDetailFormatter;
280        let payload = json!({
281            "name": "Test Artist",
282            "followers": {"total": 1000},
283            "genres": ["rock"],
284            "popularity": 75
285        });
286        formatter.format(&payload, "Artist");
287    }
288
289    #[test]
290    fn playlist_detail_format_runs() {
291        let formatter = PlaylistDetailFormatter;
292        let payload = json!({
293            "name": "Test Playlist",
294            "owner": {"display_name": "User"},
295            "tracks": {"total": 10, "items": []},
296            "public": true
297        });
298        formatter.format(&payload, "Playlist");
299    }
300
301    #[test]
302    fn user_profile_format_runs() {
303        let formatter = UserProfileFormatter;
304        let payload = json!({
305            "display_name": "Test User",
306            "product": "premium",
307            "followers": {"total": 100},
308            "id": "user123"
309        });
310        formatter.format(&payload, "User");
311    }
312
313    #[test]
314    fn category_list_format_runs() {
315        let formatter = CategoryListFormatter;
316        let payload = json!({
317            "categories": {
318                "items": [{"id": "pop", "name": "Pop"}]
319            }
320        });
321        formatter.format(&payload, "Categories");
322    }
323
324    #[test]
325    fn category_detail_format_runs() {
326        let formatter = CategoryDetailFormatter;
327        let payload = json!({
328            "id": "pop",
329            "name": "Pop",
330            "icons": [{"url": "http://example.com/icon.png"}]
331        });
332        formatter.format(&payload, "Category");
333    }
334
335    #[test]
336    fn artist_detail_supports_related_artists() {
337        let formatter = ArtistDetailFormatter;
338        let kinds = formatter.supported_kinds();
339        assert!(kinds.contains(&PayloadKind::Artist));
340        assert!(kinds.contains(&PayloadKind::RelatedArtists));
341    }
342
343    #[test]
344    fn track_detail_does_not_match_incomplete() {
345        let formatter = TrackDetailFormatter;
346        let payload = json!({ "album": {} });
347        assert!(!formatter.matches(&payload));
348    }
349
350    #[test]
351    fn category_detail_does_not_match_with_owner() {
352        let formatter = CategoryDetailFormatter;
353        let payload = json!({ "icons": [], "id": "test", "owner": {} });
354        assert!(!formatter.matches(&payload));
355    }
356}