sonos-api 0.3.0

Type-safe Sonos API for UPnP device control via SOAP
Documentation
//! Common event types and framework for Sonos UPnP events
//!
//! This module provides the core event infrastructure that is service-agnostic.
//! Service-specific event types are defined in their respective service modules.

use crate::{Result, Service};
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use std::time::{Duration, SystemTime};

/// An enriched event that includes context and source information
#[derive(Debug, Clone)]
pub struct EnrichedEvent<T> {
    /// Registration ID this event belongs to (for sonos-stream integration)
    pub registration_id: Option<u64>,

    /// IP address of the speaker that generated this event
    pub speaker_ip: IpAddr,

    /// UPnP service that generated this event
    pub service: Service,

    /// Source of this event (UPnP notification or polling)
    pub event_source: EventSource,

    /// Timestamp when this event was processed
    pub timestamp: SystemTime,

    /// The actual service-specific event data
    pub event_data: T,
}

impl<T> EnrichedEvent<T> {
    /// Create a new enriched event
    pub fn new(
        speaker_ip: IpAddr,
        service: Service,
        event_source: EventSource,
        event_data: T,
    ) -> Self {
        Self {
            registration_id: None,
            speaker_ip,
            service,
            event_source,
            timestamp: SystemTime::now(),
            event_data,
        }
    }

    /// Create a new enriched event with registration ID (for sonos-stream integration)
    pub fn with_registration_id(
        registration_id: u64,
        speaker_ip: IpAddr,
        service: Service,
        event_source: EventSource,
        event_data: T,
    ) -> Self {
        Self {
            registration_id: Some(registration_id),
            speaker_ip,
            service,
            event_source,
            timestamp: SystemTime::now(),
            event_data,
        }
    }

    /// Map the event data to a different type
    pub fn map<U, F>(self, f: F) -> EnrichedEvent<U>
    where
        F: FnOnce(T) -> U,
    {
        EnrichedEvent {
            registration_id: self.registration_id,
            speaker_ip: self.speaker_ip,
            service: self.service,
            event_source: self.event_source,
            timestamp: self.timestamp,
            event_data: f(self.event_data),
        }
    }
}

/// Source of an event - indicates whether it came from UPnP events or polling
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EventSource {
    /// Event came from a UPnP NOTIFY message
    UPnPNotification {
        /// UPnP subscription ID
        subscription_id: String,
    },

    /// Event was generated by polling device state
    PollingDetection {
        /// Current polling interval
        poll_interval: Duration,
    },

    /// Event was generated during resync operation
    ResyncOperation,
}

/// Trait for parsing service-specific events from XML
pub trait EventParser: Send + Sync {
    /// The event data type this parser produces
    type EventData: Send + Sync + 'static;

    /// Parse UPnP event XML and extract service-specific event data
    fn parse_upnp_event(&self, xml: &str) -> Result<Self::EventData>;

    /// Get the service type this parser handles
    fn service_type(&self) -> Service;
}

/// Registry for service-specific event parsers
#[derive(Default)]
pub struct EventParserRegistry {
    parsers: std::collections::HashMap<Service, Box<dyn EventParserDyn>>,
}

impl EventParserRegistry {
    /// Create a new empty registry
    pub fn new() -> Self {
        Self::default()
    }

    /// Register a parser for a specific service
    pub fn register<P>(&mut self, parser: P)
    where
        P: EventParser + 'static,
        P::EventData: 'static,
    {
        let service = parser.service_type();
        self.parsers
            .insert(service, Box::new(ParserWrapper::new(parser)));
    }

    /// Get a parser for a specific service
    pub fn get_parser(&self, service: &Service) -> Option<&dyn EventParserDyn> {
        self.parsers.get(service).map(|p| p.as_ref())
    }

    /// Check if a parser is registered for a service
    pub fn has_parser(&self, service: &Service) -> bool {
        self.parsers.contains_key(service)
    }

    /// Get all registered service types
    pub fn supported_services(&self) -> Vec<Service> {
        self.parsers.keys().cloned().collect()
    }
}

/// Dynamic trait for type-erased event parsing
pub trait EventParserDyn: Send + Sync {
    /// Parse event XML to a dynamic event type
    fn parse_upnp_event_dyn(&self, xml: &str) -> Result<Box<dyn std::any::Any + Send + Sync>>;

    /// Get the service type
    fn service_type(&self) -> Service;
}

/// Wrapper to convert typed EventParser to dynamic EventParserDyn
struct ParserWrapper<P> {
    parser: P,
}

impl<P> ParserWrapper<P>
where
    P: EventParser,
    P::EventData: 'static,
{
    fn new(parser: P) -> Self {
        Self { parser }
    }
}

impl<P> EventParserDyn for ParserWrapper<P>
where
    P: EventParser + Send + Sync,
    P::EventData: Send + Sync + 'static,
{
    fn parse_upnp_event_dyn(&self, xml: &str) -> Result<Box<dyn std::any::Any + Send + Sync>> {
        let event_data = self.parser.parse_upnp_event(xml)?;
        Ok(Box::new(event_data))
    }

    fn service_type(&self) -> Service {
        self.parser.service_type()
    }
}

/// Helper function to extract values from XML using basic text matching
/// This is used as a fallback when proper parsers are not available
pub fn extract_xml_value(xml: &str, tag: &str) -> Option<String> {
    let start_tag = format!("<{tag}>");
    let end_tag = format!("</{tag}>");

    if let Some(start_pos) = xml.find(&start_tag) {
        let content_start = start_pos + start_tag.len();
        if let Some(end_pos) = xml[content_start..].find(&end_tag) {
            let value = xml[content_start..content_start + end_pos].trim();
            if !value.is_empty() {
                return Some(value.to_string());
            }
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Debug, Clone, PartialEq)]
    struct TestEventData {
        value: String,
    }

    struct TestParser;

    impl EventParser for TestParser {
        type EventData = TestEventData;

        fn parse_upnp_event(&self, xml: &str) -> Result<Self::EventData> {
            Ok(TestEventData {
                value: xml.to_string(),
            })
        }

        fn service_type(&self) -> Service {
            Service::AVTransport
        }
    }

    #[test]
    fn test_enriched_event_creation() {
        let ip: IpAddr = "192.168.1.100".parse().unwrap();
        let source = EventSource::UPnPNotification {
            subscription_id: "uuid:123".to_string(),
        };
        let data = TestEventData {
            value: "test".to_string(),
        };

        let event = EnrichedEvent::new(ip, Service::AVTransport, source, data.clone());

        assert_eq!(event.speaker_ip, ip);
        assert_eq!(event.service, Service::AVTransport);
        assert_eq!(event.event_data, data);
        assert!(event.registration_id.is_none());
    }

    #[test]
    fn test_enriched_event_with_registration_id() {
        let ip: IpAddr = "192.168.1.100".parse().unwrap();
        let source = EventSource::UPnPNotification {
            subscription_id: "uuid:123".to_string(),
        };
        let data = TestEventData {
            value: "test".to_string(),
        };

        let event =
            EnrichedEvent::with_registration_id(42, ip, Service::AVTransport, source, data.clone());

        assert_eq!(event.registration_id, Some(42));
        assert_eq!(event.event_data, data);
    }

    #[test]
    fn test_event_mapping() {
        let ip: IpAddr = "192.168.1.100".parse().unwrap();
        let source = EventSource::UPnPNotification {
            subscription_id: "uuid:123".to_string(),
        };
        let data = TestEventData {
            value: "test".to_string(),
        };

        let event = EnrichedEvent::new(ip, Service::AVTransport, source, data);
        let mapped_event = event.map(|data| data.value.len());

        assert_eq!(mapped_event.event_data, 4); // "test".len()
    }

    #[test]
    fn test_parser_registry() {
        let mut registry = EventParserRegistry::new();
        assert!(!registry.has_parser(&Service::AVTransport));

        registry.register(TestParser);
        assert!(registry.has_parser(&Service::AVTransport));

        let supported = registry.supported_services();
        assert!(supported.contains(&Service::AVTransport));
    }

    #[test]
    fn test_xml_value_extraction() {
        let xml = "<Test>value</Test>";
        assert_eq!(extract_xml_value(xml, "Test"), Some("value".to_string()));

        let xml = "<Test></Test>";
        assert_eq!(extract_xml_value(xml, "Test"), None);

        let xml = "<Other>value</Other>";
        assert_eq!(extract_xml_value(xml, "Test"), None);
    }
}