use crate::operation::{ComposableOperation, UPnPOperation};
use crate::{ApiError, ManagedSubscription, Result, Service, SonosOperation};
use soap_client::SoapClient;
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct SonosClient {
soap_client: SoapClient,
}
impl SonosClient {
pub fn new() -> Self {
Self {
soap_client: SoapClient::get().clone(),
}
}
pub fn with_soap_client(soap_client: SoapClient) -> Self {
Self { soap_client }
}
pub fn execute<Op: SonosOperation>(
&self,
ip: &str,
request: &Op::Request,
) -> Result<Op::Response> {
let service_info = Op::SERVICE.info();
let payload = Op::build_payload(request);
let xml = self
.soap_client
.call(
ip,
service_info.endpoint,
service_info.service_uri,
Op::ACTION,
&payload,
)
.map_err(|e| match e {
soap_client::SoapError::Network(msg) => ApiError::NetworkError(msg),
soap_client::SoapError::Parse(msg) => ApiError::ParseError(msg),
soap_client::SoapError::Fault(code) => ApiError::SoapFault(code),
})?;
Op::parse_response(&xml)
}
pub fn execute_enhanced<Op: UPnPOperation>(
&self,
ip: &str,
operation: ComposableOperation<Op>,
) -> Result<Op::Response> {
let start_time = Instant::now();
let payload = operation
.build_payload()
.map_err(|e| ApiError::ParseError(format!("Validation error: {e}")))?;
let service_info = Op::SERVICE.info();
if let Some(timeout) = operation.timeout() {
if start_time.elapsed() >= timeout {
return Err(ApiError::NetworkError("Operation timeout".to_string()));
}
}
let xml = self
.soap_client
.call(
ip,
service_info.endpoint,
service_info.service_uri,
Op::ACTION,
&payload,
)
.map_err(|e| match e {
soap_client::SoapError::Network(msg) => ApiError::NetworkError(msg),
soap_client::SoapError::Parse(msg) => ApiError::ParseError(msg),
soap_client::SoapError::Fault(code) => ApiError::SoapFault(code),
})?;
operation.parse_response(&xml)
}
pub fn subscribe(
&self,
ip: &str,
service: Service,
callback_url: &str,
) -> Result<ManagedSubscription> {
self.create_managed_subscription(ip, service, callback_url, 1800)
}
pub fn subscribe_with_timeout(
&self,
ip: &str,
service: Service,
callback_url: &str,
timeout_seconds: u32,
) -> Result<ManagedSubscription> {
self.create_managed_subscription(ip, service, callback_url, timeout_seconds)
}
pub fn create_managed_subscription(
&self,
ip: &str,
service: Service,
callback_url: &str,
timeout_seconds: u32,
) -> Result<ManagedSubscription> {
ManagedSubscription::create(
ip.to_string(),
service,
callback_url.to_string(),
timeout_seconds,
self.soap_client.clone(),
)
}
}
impl Default for SonosClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let _client = SonosClient::new();
let _default_client = SonosClient::default();
}
#[test]
fn test_subscription_methods_signature() {
let _client = SonosClient::new();
let _subscribe_fn: fn(&SonosClient, &str, Service, &str) -> Result<ManagedSubscription> =
SonosClient::subscribe;
let _subscribe_timeout_fn: fn(
&SonosClient,
&str,
Service,
&str,
u32,
) -> Result<ManagedSubscription> = SonosClient::subscribe_with_timeout;
}
#[test]
fn test_subscription_parameters() {
let _ip = "192.168.1.100";
let _service = Service::AVTransport;
let _callback_url = "http://callback.url";
let _timeout = 3600u32;
assert_eq!(Service::AVTransport as i32, Service::AVTransport as i32);
assert_eq!(
Service::RenderingControl as i32,
Service::RenderingControl as i32
);
}
#[test]
fn test_subscription_delegates_to_create_managed() {
let _client = SonosClient::new();
let _subscription_fn = |client: &SonosClient| {
client.subscribe("192.168.1.100", Service::AVTransport, "http://callback")
};
let _timeout_subscription_fn = |client: &SonosClient| {
client.subscribe_with_timeout(
"192.168.1.100",
Service::AVTransport,
"http://callback",
1800,
)
};
}
}