spotify_cli/io/registry/
mod.rs1mod lists;
8mod media;
9mod player;
10mod resources;
11mod search;
12
13use serde_json::Value;
14use std::sync::LazyLock;
15
16use super::output::PayloadKind;
17
18pub use lists::*;
20pub use media::*;
21pub use player::*;
22pub use resources::*;
23pub use search::*;
24
25pub trait PayloadFormatter: Send + Sync {
27 fn name(&self) -> &'static str;
29
30 fn supported_kinds(&self) -> &'static [PayloadKind] {
32 &[] }
34
35 fn matches(&self, payload: &Value) -> bool;
37
38 fn format(&self, payload: &Value, message: &str);
40}
41
42pub struct FormatterRegistry {
44 formatters: Vec<Box<dyn PayloadFormatter>>,
45}
46
47impl FormatterRegistry {
48 pub fn new() -> Self {
49 let mut registry = Self {
50 formatters: Vec::new(),
51 };
52
53 registry.register(Box::new(PlayerStatusFormatter));
55 registry.register(Box::new(QueueFormatter));
56 registry.register(Box::new(DevicesFormatter));
57 registry.register(Box::new(PlayHistoryFormatter));
58
59 registry.register(Box::new(CombinedSearchFormatter));
61 registry.register(Box::new(SpotifySearchFormatter));
62 registry.register(Box::new(PinsFormatter));
63
64 registry.register(Box::new(TrackDetailFormatter));
66 registry.register(Box::new(AlbumDetailFormatter));
67 registry.register(Box::new(ArtistDetailFormatter));
68 registry.register(Box::new(PlaylistDetailFormatter));
69 registry.register(Box::new(UserProfileFormatter));
70
71 registry.register(Box::new(CategoryListFormatter));
73 registry.register(Box::new(CategoryDetailFormatter));
74
75 registry.register(Box::new(ShowDetailFormatter));
77 registry.register(Box::new(EpisodeDetailFormatter));
78 registry.register(Box::new(AudiobookDetailFormatter));
79 registry.register(Box::new(ChapterDetailFormatter));
80
81 registry.register(Box::new(PlaylistsFormatter));
83 registry.register(Box::new(SavedTracksFormatter));
84 registry.register(Box::new(SavedAlbumsFormatter)); registry.register(Box::new(SavedShowsFormatter));
86 registry.register(Box::new(ShowEpisodesFormatter));
87 registry.register(Box::new(SavedEpisodesFormatter));
88 registry.register(Box::new(SavedAudiobooksFormatter));
89 registry.register(Box::new(AudiobookChaptersFormatter));
90 registry.register(Box::new(TopTracksFormatter));
91 registry.register(Box::new(TopArtistsFormatter));
92 registry.register(Box::new(ArtistTopTracksFormatter));
93 registry.register(Box::new(LibraryCheckFormatter));
94 registry.register(Box::new(MarketsFormatter));
95
96 registry
97 }
98
99 fn register(&mut self, formatter: Box<dyn PayloadFormatter>) {
100 self.formatters.push(formatter);
101 }
102
103 pub fn format(&self, payload: &Value, message: &str) {
105 self.format_with_kind(payload, message, None);
106 }
107
108 pub fn format_with_kind(&self, payload: &Value, message: &str, kind: Option<PayloadKind>) {
110 if let Some(kind) = kind {
112 for formatter in &self.formatters {
113 if formatter.supported_kinds().contains(&kind) {
114 formatter.format(payload, message);
115 return;
116 }
117 }
118 }
119
120 for formatter in &self.formatters {
122 if formatter.matches(payload) {
123 formatter.format(payload, message);
124 return;
125 }
126 }
127 println!("{}", message);
128 }
129
130 #[cfg(test)]
132 pub fn len(&self) -> usize {
133 self.formatters.len()
134 }
135
136 #[cfg(test)]
138 pub fn is_empty(&self) -> bool {
139 self.formatters.is_empty()
140 }
141}
142
143impl Default for FormatterRegistry {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149pub static REGISTRY: LazyLock<FormatterRegistry> = LazyLock::new(FormatterRegistry::new);
150
151pub fn format_payload(payload: &Value, message: &str) {
153 REGISTRY.format(payload, message);
154}
155
156pub fn format_payload_with_kind(payload: &Value, message: &str, kind: Option<PayloadKind>) {
158 REGISTRY.format_with_kind(payload, message, kind);
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use serde_json::json;
165
166 #[test]
167 fn formatter_registry_has_formatters() {
168 let registry = FormatterRegistry::new();
169 assert!(!registry.is_empty());
170 }
171
172 #[test]
173 fn registry_format_with_kind_uses_kind_matching() {
174 let registry = FormatterRegistry::new();
175 let payload = json!({ "item": { "name": "Test" }, "is_playing": true });
176 registry.format_with_kind(&payload, "Test", Some(PayloadKind::PlayerStatus));
177 }
178
179 #[test]
180 fn registry_format_with_kind_falls_back_to_payload_matching() {
181 let registry = FormatterRegistry::new();
182 let payload = json!({ "item": { "name": "Test" }, "is_playing": true });
183 registry.format_with_kind(&payload, "Test", None);
184 }
185
186 #[test]
187 fn registry_format_with_unknown_prints_message() {
188 let registry = FormatterRegistry::new();
189 let payload = json!({ "unknown_field": "value" });
190 registry.format(&payload, "No match found");
191 }
192
193 #[test]
194 fn global_registry_accessible() {
195 let _ = &*REGISTRY;
196 }
197
198 #[test]
199 fn format_payload_works() {
200 let payload = json!({ "unknown": "data" });
201 format_payload(&payload, "Test message");
202 }
203
204 #[test]
205 fn format_payload_with_kind_works() {
206 let payload = json!({ "unknown": "data" });
207 format_payload_with_kind(&payload, "Test message", None);
208 }
209
210 #[test]
211 fn registry_default_same_as_new() {
212 let default_registry = FormatterRegistry::default();
213 let new_registry = FormatterRegistry::new();
214 assert_eq!(default_registry.len(), new_registry.len());
215 }
216
217 #[test]
218 fn default_supported_kinds_is_empty() {
219 struct TestFormatter;
220 impl PayloadFormatter for TestFormatter {
221 fn name(&self) -> &'static str {
222 "test"
223 }
224 fn matches(&self, _: &Value) -> bool {
225 false
226 }
227 fn format(&self, _: &Value, _: &str) {}
228 }
229 let formatter = TestFormatter;
230 assert!(formatter.supported_kinds().is_empty());
231 }
232}