spotify_cli/io/registry/
search.rs

1//! Search-related formatters (combined search, Spotify search, pins)
2
3use serde_json::Value;
4
5use super::PayloadFormatter;
6use crate::io::formatters;
7use crate::io::output::PayloadKind;
8
9pub struct CombinedSearchFormatter;
10
11impl PayloadFormatter for CombinedSearchFormatter {
12    fn name(&self) -> &'static str {
13        "combined_search"
14    }
15
16    fn supported_kinds(&self) -> &'static [PayloadKind] {
17        &[PayloadKind::CombinedSearch]
18    }
19
20    fn matches(&self, payload: &Value) -> bool {
21        payload.get("spotify").is_some()
22    }
23
24    fn format(&self, payload: &Value, _message: &str) {
25        formatters::format_search_results(payload);
26    }
27}
28
29pub struct SpotifySearchFormatter;
30
31impl PayloadFormatter for SpotifySearchFormatter {
32    fn name(&self) -> &'static str {
33        "spotify_search"
34    }
35
36    fn supported_kinds(&self) -> &'static [PayloadKind] {
37        &[PayloadKind::SearchResults]
38    }
39
40    fn matches(&self, payload: &Value) -> bool {
41        let is_playlist = payload.get("owner").is_some();
42        let is_album = payload.get("album_type").is_some();
43        !is_playlist
44            && !is_album
45            && ((payload.get("tracks").is_some() && payload["tracks"].get("items").is_some())
46                || (payload.get("albums").is_some() && payload["albums"].get("items").is_some())
47                || (payload.get("artists").is_some() && payload["artists"].get("items").is_some())
48                || (payload.get("playlists").is_some()
49                    && payload["playlists"].get("items").is_some()))
50    }
51
52    fn format(&self, payload: &Value, _message: &str) {
53        formatters::format_search_results(payload);
54    }
55}
56
57pub struct PinsFormatter;
58
59impl PayloadFormatter for PinsFormatter {
60    fn name(&self) -> &'static str {
61        "pins"
62    }
63
64    fn supported_kinds(&self) -> &'static [PayloadKind] {
65        &[PayloadKind::Pins]
66    }
67
68    fn matches(&self, payload: &Value) -> bool {
69        payload.get("pins").is_some() && payload.get("spotify").is_none()
70    }
71
72    fn format(&self, payload: &Value, _message: &str) {
73        if let Some(pins) = payload.get("pins").and_then(|p| p.as_array()) {
74            formatters::format_pins(pins);
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use serde_json::json;
83
84    #[test]
85    fn combined_search_formatter_matches() {
86        let formatter = CombinedSearchFormatter;
87        let payload = json!({ "spotify": { "tracks": {} } });
88        assert!(formatter.matches(&payload));
89        assert!(!formatter.matches(&json!({})));
90    }
91
92    #[test]
93    fn pins_formatter_matches() {
94        let formatter = PinsFormatter;
95        let payload = json!({ "pins": [] });
96        assert!(formatter.matches(&payload));
97        let payload_with_spotify = json!({ "pins": [], "spotify": {} });
98        assert!(!formatter.matches(&payload_with_spotify));
99    }
100
101    #[test]
102    fn spotify_search_formatter_matches() {
103        let formatter = SpotifySearchFormatter;
104        let tracks = json!({ "tracks": { "items": [] } });
105        assert!(formatter.matches(&tracks));
106        let albums = json!({ "albums": { "items": [] } });
107        assert!(formatter.matches(&albums));
108        let artists = json!({ "artists": { "items": [] } });
109        assert!(formatter.matches(&artists));
110        let playlists = json!({ "playlists": { "items": [] } });
111        assert!(formatter.matches(&playlists));
112        // Should not match playlist or album detail
113        let playlist = json!({ "owner": {}, "tracks": { "items": [] } });
114        assert!(!formatter.matches(&playlist));
115        let album = json!({ "album_type": "album", "tracks": { "items": [] } });
116        assert!(!formatter.matches(&album));
117    }
118
119    #[test]
120    fn formatter_names() {
121        assert_eq!(CombinedSearchFormatter.name(), "combined_search");
122        assert_eq!(SpotifySearchFormatter.name(), "spotify_search");
123        assert_eq!(PinsFormatter.name(), "pins");
124    }
125
126    #[test]
127    fn combined_search_format_runs() {
128        let formatter = CombinedSearchFormatter;
129        let payload = json!({
130            "spotify": {
131                "tracks": { "items": [] },
132                "artists": { "items": [] }
133            },
134            "pins": []
135        });
136        formatter.format(&payload, "Search Results");
137    }
138
139    #[test]
140    fn spotify_search_format_runs() {
141        let formatter = SpotifySearchFormatter;
142        let payload = json!({
143            "tracks": {
144                "items": [{"name": "Track", "artists": [{"name": "Artist"}]}]
145            }
146        });
147        formatter.format(&payload, "Tracks");
148    }
149
150    #[test]
151    fn pins_format_runs() {
152        let formatter = PinsFormatter;
153        let payload = json!({
154            "pins": [{
155                "alias": "my-song",
156                "resource_type": "track",
157                "id": "abc123"
158            }]
159        });
160        formatter.format(&payload, "Pins");
161    }
162
163    #[test]
164    fn combined_search_supports_kind() {
165        let formatter = CombinedSearchFormatter;
166        assert!(
167            formatter
168                .supported_kinds()
169                .contains(&PayloadKind::CombinedSearch)
170        );
171    }
172
173    #[test]
174    fn spotify_search_supports_kind() {
175        let formatter = SpotifySearchFormatter;
176        assert!(
177            formatter
178                .supported_kinds()
179                .contains(&PayloadKind::SearchResults)
180        );
181    }
182
183    #[test]
184    fn pins_supports_kind() {
185        let formatter = PinsFormatter;
186        assert!(formatter.supported_kinds().contains(&PayloadKind::Pins));
187    }
188
189    #[test]
190    fn pins_format_handles_empty_array() {
191        let formatter = PinsFormatter;
192        let payload = json!({ "pins": [] });
193        formatter.format(&payload, "No pins");
194    }
195}