spotify_cli/io/registry/
resources.rs1use 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}