mod lists;
mod media;
mod player;
mod resources;
mod search;
use serde_json::Value;
use std::sync::LazyLock;
use super::output::PayloadKind;
pub use lists::*;
pub use media::*;
pub use player::*;
pub use resources::*;
pub use search::*;
pub trait PayloadFormatter: Send + Sync {
fn name(&self) -> &'static str;
fn supported_kinds(&self) -> &'static [PayloadKind] {
&[] }
fn matches(&self, payload: &Value) -> bool;
fn format(&self, payload: &Value, message: &str);
}
pub struct FormatterRegistry {
formatters: Vec<Box<dyn PayloadFormatter>>,
}
impl FormatterRegistry {
pub fn new() -> Self {
let mut registry = Self {
formatters: Vec::new(),
};
registry.register(Box::new(PlayerStatusFormatter));
registry.register(Box::new(QueueFormatter));
registry.register(Box::new(DevicesFormatter));
registry.register(Box::new(PlayHistoryFormatter));
registry.register(Box::new(CombinedSearchFormatter));
registry.register(Box::new(SpotifySearchFormatter));
registry.register(Box::new(PinsFormatter));
registry.register(Box::new(TrackDetailFormatter));
registry.register(Box::new(AlbumDetailFormatter));
registry.register(Box::new(ArtistDetailFormatter));
registry.register(Box::new(PlaylistDetailFormatter));
registry.register(Box::new(UserProfileFormatter));
registry.register(Box::new(CategoryListFormatter));
registry.register(Box::new(CategoryDetailFormatter));
registry.register(Box::new(ShowDetailFormatter));
registry.register(Box::new(EpisodeDetailFormatter));
registry.register(Box::new(AudiobookDetailFormatter));
registry.register(Box::new(ChapterDetailFormatter));
registry.register(Box::new(PlaylistsFormatter));
registry.register(Box::new(SavedTracksFormatter));
registry.register(Box::new(SavedAlbumsFormatter)); registry.register(Box::new(SavedShowsFormatter));
registry.register(Box::new(ShowEpisodesFormatter));
registry.register(Box::new(SavedEpisodesFormatter));
registry.register(Box::new(SavedAudiobooksFormatter));
registry.register(Box::new(AudiobookChaptersFormatter));
registry.register(Box::new(TopTracksFormatter));
registry.register(Box::new(TopArtistsFormatter));
registry.register(Box::new(ArtistTopTracksFormatter));
registry.register(Box::new(LibraryCheckFormatter));
registry.register(Box::new(MarketsFormatter));
registry
}
fn register(&mut self, formatter: Box<dyn PayloadFormatter>) {
self.formatters.push(formatter);
}
pub fn format(&self, payload: &Value, message: &str) {
self.format_with_kind(payload, message, None);
}
pub fn format_with_kind(&self, payload: &Value, message: &str, kind: Option<PayloadKind>) {
if let Some(kind) = kind {
for formatter in &self.formatters {
if formatter.supported_kinds().contains(&kind) {
formatter.format(payload, message);
return;
}
}
}
for formatter in &self.formatters {
if formatter.matches(payload) {
formatter.format(payload, message);
return;
}
}
println!("{}", message);
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.formatters.len()
}
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.formatters.is_empty()
}
}
impl Default for FormatterRegistry {
fn default() -> Self {
Self::new()
}
}
pub static REGISTRY: LazyLock<FormatterRegistry> = LazyLock::new(FormatterRegistry::new);
pub fn format_payload(payload: &Value, message: &str) {
REGISTRY.format(payload, message);
}
pub fn format_payload_with_kind(payload: &Value, message: &str, kind: Option<PayloadKind>) {
REGISTRY.format_with_kind(payload, message, kind);
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn formatter_registry_has_formatters() {
let registry = FormatterRegistry::new();
assert!(!registry.is_empty());
}
#[test]
fn registry_format_with_kind_uses_kind_matching() {
let registry = FormatterRegistry::new();
let payload = json!({ "item": { "name": "Test" }, "is_playing": true });
registry.format_with_kind(&payload, "Test", Some(PayloadKind::PlayerStatus));
}
#[test]
fn registry_format_with_kind_falls_back_to_payload_matching() {
let registry = FormatterRegistry::new();
let payload = json!({ "item": { "name": "Test" }, "is_playing": true });
registry.format_with_kind(&payload, "Test", None);
}
#[test]
fn registry_format_with_unknown_prints_message() {
let registry = FormatterRegistry::new();
let payload = json!({ "unknown_field": "value" });
registry.format(&payload, "No match found");
}
#[test]
fn global_registry_accessible() {
let _ = &*REGISTRY;
}
#[test]
fn format_payload_works() {
let payload = json!({ "unknown": "data" });
format_payload(&payload, "Test message");
}
#[test]
fn format_payload_with_kind_works() {
let payload = json!({ "unknown": "data" });
format_payload_with_kind(&payload, "Test message", None);
}
#[test]
fn registry_default_same_as_new() {
let default_registry = FormatterRegistry::default();
let new_registry = FormatterRegistry::new();
assert_eq!(default_registry.len(), new_registry.len());
}
#[test]
fn default_supported_kinds_is_empty() {
struct TestFormatter;
impl PayloadFormatter for TestFormatter {
fn name(&self) -> &'static str {
"test"
}
fn matches(&self, _: &Value) -> bool {
false
}
fn format(&self, _: &Value, _: &str) {}
}
let formatter = TestFormatter;
assert!(formatter.supported_kinds().is_empty());
}
}