spotify_cli/io/registry/
mod.rs

1//! Formatter registry for type-safe payload formatting
2//!
3//! This module contains the registry pattern for payload formatters.
4//! Each formatter declares its supported `PayloadKind`s for fast matching,
5//! with fallback payload inspection for legacy compatibility.
6
7mod lists;
8mod media;
9mod player;
10mod resources;
11mod search;
12
13use serde_json::Value;
14use std::sync::LazyLock;
15
16use super::output::PayloadKind;
17
18// Re-export formatter structs for registration
19pub use lists::*;
20pub use media::*;
21pub use player::*;
22pub use resources::*;
23pub use search::*;
24
25/// Trait for payload formatters
26pub trait PayloadFormatter: Send + Sync {
27    /// Unique identifier for this formatter (for debugging)
28    fn name(&self) -> &'static str;
29
30    /// PayloadKind(s) this formatter handles (preferred matching method)
31    fn supported_kinds(&self) -> &'static [PayloadKind] {
32        &[] // Default: no type-based matching, use payload inspection
33    }
34
35    /// Check if this formatter can handle the payload (fallback matching)
36    fn matches(&self, payload: &Value) -> bool;
37
38    /// Format and print the payload
39    fn format(&self, payload: &Value, message: &str);
40}
41
42/// Registry holding all formatters in priority order
43pub 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        // Player formatters
54        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        // Search formatters
60        registry.register(Box::new(CombinedSearchFormatter));
61        registry.register(Box::new(SpotifySearchFormatter));
62        registry.register(Box::new(PinsFormatter));
63
64        // Resource detail formatters
65        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        // Category formatters
72        registry.register(Box::new(CategoryListFormatter));
73        registry.register(Box::new(CategoryDetailFormatter));
74
75        // Media formatters (podcasts, audiobooks)
76        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        // List formatters
82        registry.register(Box::new(PlaylistsFormatter));
83        registry.register(Box::new(SavedTracksFormatter));
84        registry.register(Box::new(SavedAlbumsFormatter)); // Must be before TopTracksFormatter
85        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    /// Format the payload using the first matching formatter (legacy, uses payload inspection)
104    pub fn format(&self, payload: &Value, message: &str) {
105        self.format_with_kind(payload, message, None);
106    }
107
108    /// Format the payload, optionally using a type hint for reliable matching.
109    pub fn format_with_kind(&self, payload: &Value, message: &str, kind: Option<PayloadKind>) {
110        // If a kind is provided, try type-based matching first (fast path)
111        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        // Fall back to payload inspection (slow path, for backward compatibility)
121        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    /// Get the number of registered formatters (for testing)
131    #[cfg(test)]
132    pub fn len(&self) -> usize {
133        self.formatters.len()
134    }
135
136    /// Check if registry is empty (for testing)
137    #[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
151/// Format a payload using the global registry (legacy, uses payload inspection)
152pub fn format_payload(payload: &Value, message: &str) {
153    REGISTRY.format(payload, message);
154}
155
156/// Format a payload with optional type hint for reliable matching.
157pub 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}