-
Notifications
You must be signed in to change notification settings - Fork 26
fix(xml): XML deserialization to handle empty nodes #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,6 @@ | |
| //! - <https://pyrekordbox.readthedocs.io/en/stable/formats/xml.html> | ||
| type NaiveDate = String; //Replace with "use chrono::naive::NaiveDate;" | ||
| use serde::{de::Error, ser::Serializer, Deserialize, Serialize}; | ||
| use std::borrow::Cow; | ||
|
|
||
| /// The XML root element of a rekordbox XML file. | ||
| #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] | ||
|
|
@@ -251,110 +250,47 @@ impl<'de> Deserialize<'de> for PlaylistGenericNode { | |
| where | ||
| D: serde::Deserializer<'de>, | ||
| { | ||
| struct PlaylistGenericNodeVisitor; | ||
|
|
||
| impl<'de> serde::de::Visitor<'de> for PlaylistGenericNodeVisitor { | ||
| type Value = PlaylistGenericNode; | ||
|
|
||
| fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| formatter.write_str("struct PlaylistGenericNode") | ||
| } | ||
|
|
||
| fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> | ||
| where | ||
| A: serde::de::MapAccess<'de>, | ||
| { | ||
| let mut node_type = None; | ||
| let mut name = None; | ||
| let mut count = None; | ||
| let mut key_type = None; | ||
| let mut entries = None; | ||
|
|
||
| while let Some(key) = map.next_key::<Cow<'_, str>>()? { | ||
| match key.as_ref() { | ||
| "@Name" => name = map.next_value::<Cow<'_, str>>()?.into(), | ||
| "@Type" => node_type = map.next_value::<Cow<'_, str>>()?.into(), | ||
| "@Count" => count = map.next_value::<usize>()?.into(), | ||
| "@KeyType" => key_type = map.next_value::<Cow<'_, str>>()?.into(), | ||
| "@Entries" => entries = map.next_value::<usize>()?.into(), | ||
| unknown => { | ||
| return Err(A::Error::unknown_field( | ||
| unknown, | ||
| &["@Name", "@Type", "@Count", "@KeyType", "@Entries"], | ||
| )); | ||
| } | ||
| } | ||
| // stores a node with fields for a playlist or folder | ||
| #[derive(Deserialize)] | ||
| struct Node { | ||
| #[serde(rename = "@Name")] | ||
| name: String, | ||
| // indicates playlist or folder | ||
| #[serde(rename = "@Type")] | ||
| node_type: String, | ||
| // appears on playlists only | ||
| #[serde(rename = "@KeyType", default)] | ||
| key_type: Option<String>, | ||
| // child nodes in a folder | ||
| #[serde(rename = "NODE", default)] | ||
| nodes: Vec<PlaylistGenericNode>, | ||
| // tracks in a playlist | ||
| #[serde(rename = "TRACK", default)] | ||
| tracks: Vec<PlaylistTrack>, | ||
| } | ||
|
|
||
| match node_type.as_deref() { | ||
| Some("0") => { | ||
| if let (Some(n), Some(_c)) = (&name, count) { | ||
| let nodes = { | ||
| // Create anonymous type | ||
| #[derive(serde::Deserialize)] | ||
| struct Nodes { | ||
| #[serde(rename = "NODE")] | ||
| content: Vec<PlaylistGenericNode>, | ||
| } | ||
| let de = serde::de::value::MapAccessDeserializer::new(map); | ||
| Nodes::deserialize(de)?.content | ||
| }; | ||
| // FIXME: Should we check if nodes.len() == count here? | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should I restore the I could also add this validation, but I was unsure what type of error to return (or would we rather panic?)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah please add it if feasible. Maybe just add a (non-fatal) warning though, as we don't control the XML schema, we want to be maximally compatible |
||
| return Ok(PlaylistGenericNode::Folder(PlaylistFolderNode { | ||
| name: n.to_string(), | ||
| nodes, | ||
| })); | ||
| } | ||
| } | ||
| Some("1") => { | ||
| if let (Some(n), Some(_c), Some(t)) = (&name, entries, &key_type) { | ||
| let tracks = { | ||
| // Create anonymous type | ||
| #[derive(serde::Deserialize)] | ||
| struct Tracks { | ||
| #[serde(rename = "TRACK")] | ||
| content: Vec<PlaylistTrack>, | ||
| } | ||
| let de = serde::de::value::MapAccessDeserializer::new(map); | ||
| Tracks::deserialize(de)?.content | ||
| }; | ||
| // FIXME: Should we check if nodes.len() == count here? | ||
| return Ok(PlaylistGenericNode::Playlist(PlaylistPlaylistNode { | ||
| name: n.to_string(), | ||
| keytype: t.to_string(), | ||
| tracks, | ||
| })); | ||
| } | ||
| } | ||
| Some(unknown) => { | ||
| return Err(A::Error::unknown_variant(unknown, &["0", "1"])) | ||
| } | ||
| None => (), | ||
| } | ||
| } | ||
| let node = Node::deserialize(deserializer)?; | ||
|
|
||
| match node_type.as_deref() { | ||
| Some("0") => { | ||
| if name.is_none() { | ||
| Err(A::Error::missing_field("@Name")) | ||
| } else { | ||
| Err(A::Error::missing_field("@Count")) | ||
| } | ||
| } | ||
| Some("1") => { | ||
| if name.is_none() { | ||
| Err(A::Error::missing_field("@Name")) | ||
| } else if entries.is_none() { | ||
| Err(A::Error::missing_field("@Entries")) | ||
| } else { | ||
| Err(A::Error::missing_field("@KeyType")) | ||
| } | ||
| } | ||
| _ => Err(A::Error::missing_field("@Type")), | ||
| match node.node_type.as_str() { | ||
| // Folder node | ||
| "0" => Ok(PlaylistGenericNode::Folder(PlaylistFolderNode { | ||
| name: node.name, | ||
| nodes: node.nodes, | ||
| })), | ||
| // Playlist node | ||
| "1" => { | ||
| if let Some(key_type) = node.key_type { | ||
| Ok(PlaylistGenericNode::Playlist(PlaylistPlaylistNode { | ||
| name: node.name, | ||
| keytype: key_type, | ||
| tracks: node.tracks, | ||
| })) | ||
| } else { | ||
| Err(D::Error::missing_field("@KeyType")) | ||
|
Comment on lines
+274
to
+289
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not actually validate that the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm unfortunately very unfamiliar with the XML code, so I would prefer to stick as close as possible to the previous implementation (which was quite strict from what I can tell). |
||
| } | ||
| } | ||
| t => Err(D::Error::unknown_variant(t, &["0", "1"])), | ||
| } | ||
|
|
||
| deserializer.deserialize_map(PlaylistGenericNodeVisitor) | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this does not deserialize
CountorEntriesbecause this iteration does not actually use these.