use crate::{ApiError, Result};
use serde::de::{DeserializeOwned, Deserializer};
use serde::{Deserialize, Serialize};
pub fn parse<T: DeserializeOwned>(xml: &str) -> Result<T> {
quick_xml::de::from_str(xml)
.map_err(|e| ApiError::ParseError(format!("XML deserialization failed: {e}")))
}
pub fn deserialize_nested<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned,
{
let s = String::deserialize(deserializer)?;
parse::<T>(&s).map_err(serde::de::Error::custom)
}
pub fn deserialize_zone_group_state<'de, D, T>(
deserializer: D,
) -> std::result::Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned,
{
let s = String::deserialize(deserializer)?;
if s.trim().is_empty() {
return Ok(None);
}
let parsed = parse::<T>(&s).map_err(serde::de::Error::custom)?;
Ok(Some(parsed))
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct ValueAttribute {
#[serde(rename = "@val", default)]
pub val: String,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct NestedAttribute<T> {
pub val: Option<T>,
}
impl<'de, T: DeserializeOwned> Deserialize<'de> for NestedAttribute<T> {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawAttr {
#[serde(rename = "@val", default)]
val: String,
}
let raw = RawAttr::deserialize(deserializer)?;
if raw.val.is_empty() {
return Ok(NestedAttribute { val: None });
}
match parse::<T>(&raw.val) {
Ok(parsed) => Ok(NestedAttribute { val: Some(parsed) }),
Err(_) => Ok(NestedAttribute { val: None }),
}
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename = "DIDL-Lite")]
pub struct DidlLite {
#[serde(rename = "item", default)]
pub items: Vec<DidlItem>,
}
impl DidlLite {
pub fn from_xml(xml: &str) -> Result<Self> {
parse(xml)
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct DidlItem {
#[serde(rename = "@id", default)]
pub id: String,
#[serde(rename = "@parentID", default)]
pub parent_id: String,
#[serde(rename = "@restricted", default)]
pub restricted: Option<String>,
#[serde(rename = "res", default)]
pub resources: Vec<DidlResource>,
#[serde(rename = "albumArtURI", default)]
pub album_art_uri: Option<String>,
#[serde(rename = "class", default)]
pub class: Option<String>,
#[serde(rename = "title", default)]
pub title: Option<String>,
#[serde(rename = "creator", default)]
pub creator: Option<String>,
#[serde(rename = "album", default)]
pub album: Option<String>,
#[serde(rename = "streamInfo", default)]
pub stream_info: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
pub struct DidlResource {
#[serde(rename = "@duration", default)]
pub duration: Option<String>,
#[serde(rename = "@protocolInfo", default)]
pub protocol_info: Option<String>,
#[serde(rename = "$value", default)]
pub uri: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_survives_cdata_and_comment_containing_gt() {
let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0"><!-- a > b --><e:property><TransportState val="PLAYING"/><Note><![CDATA[3 > 2 && <ok>]]></Note></e:property></e:propertyset>"#;
#[derive(Debug, Deserialize)]
struct PropertySet {
property: Property,
}
#[derive(Debug, Deserialize)]
struct Property {
#[serde(rename = "TransportState")]
transport_state: ValueAttribute,
#[serde(rename = "Note")]
note: String,
}
let parsed: PropertySet = parse(xml).unwrap();
assert_eq!(parsed.property.transport_state.val, "PLAYING");
assert_eq!(parsed.property.note, "3 > 2 && <ok>");
}
#[test]
fn test_value_attribute_deserialize() {
let xml = r#"<Root><TransportState val="PLAYING"/></Root>"#;
#[derive(Debug, Deserialize)]
struct Root {
#[serde(rename = "TransportState")]
transport_state: ValueAttribute,
}
let result: Root = parse(xml).unwrap();
assert_eq!(result.transport_state.val, "PLAYING");
}
#[test]
fn test_value_attribute_empty() {
let xml = r#"<Root><TransportState val=""/></Root>"#;
#[derive(Debug, Deserialize)]
struct Root {
#[serde(rename = "TransportState")]
transport_state: ValueAttribute,
}
let result: Root = parse(xml).unwrap();
assert_eq!(result.transport_state.val, "");
}
#[test]
fn test_value_attribute_default() {
let xml = r#"<Root><TransportState/></Root>"#;
#[derive(Debug, Deserialize)]
struct Root {
#[serde(rename = "TransportState")]
transport_state: ValueAttribute,
}
let result: Root = parse(xml).unwrap();
assert_eq!(result.transport_state.val, "");
}
#[test]
fn test_parse_didl_lite_basic() {
let didl_xml = r#"<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"><item id="-1" parentID="-1"><dc:title>Test Song</dc:title><dc:creator>Test Artist</dc:creator><upnp:album>Test Album</upnp:album></item></DIDL-Lite>"#;
let result = DidlLite::from_xml(didl_xml);
assert!(
result.is_ok(),
"Failed to parse DIDL-Lite: {:?}",
result.err()
);
let didl = result.unwrap();
assert_eq!(didl.items.len(), 1);
let item = &didl.items[0];
assert_eq!(item.id, "-1");
assert_eq!(item.parent_id, "-1");
assert_eq!(item.title, Some("Test Song".to_string()));
assert_eq!(item.creator, Some("Test Artist".to_string()));
assert_eq!(item.album, Some("Test Album".to_string()));
}
#[test]
fn test_parse_didl_lite_with_resource() {
let didl_xml = r#"<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/"><item id="-1" parentID="-1"><dc:title>Song</dc:title><dc:creator>Artist</dc:creator><res duration="0:03:58" protocolInfo="http-get:*:audio/mpeg:*">http://example.com/song.mp3</res></item></DIDL-Lite>"#;
let result = DidlLite::from_xml(didl_xml);
assert!(
result.is_ok(),
"Failed to parse DIDL-Lite with resource: {:?}",
result.err()
);
let didl = result.unwrap();
let item = &didl.items[0];
assert_eq!(item.title, Some("Song".to_string()));
assert_eq!(item.creator, Some("Artist".to_string()));
let res = &item.resources[0];
assert_eq!(res.duration, Some("0:03:58".to_string()));
assert_eq!(
res.protocol_info,
Some("http-get:*:audio/mpeg:*".to_string())
);
assert_eq!(res.uri, Some("http://example.com/song.mp3".to_string()));
}
#[test]
fn test_parse_didl_lite_minimal() {
let didl_xml = r#"<DIDL-Lite><item id="1" parentID="0"></item></DIDL-Lite>"#;
let result = DidlLite::from_xml(didl_xml);
assert!(
result.is_ok(),
"Failed to parse minimal DIDL-Lite: {:?}",
result.err()
);
let didl = result.unwrap();
let item = &didl.items[0];
assert_eq!(item.id, "1");
assert_eq!(item.parent_id, "0");
assert_eq!(item.title, None);
assert_eq!(item.creator, None);
assert_eq!(item.album, None);
}
}