xspf 0.4.2

straightforward pure rust implementation of the Xml Sharable Playlist Format
Documentation
use crate::common::{Playlist, Attribution, Meta, Extension};

use std::{
	io::Write,
	borrow::Cow,
	path::Path,
	fs::File,
};
use xml::{
	common::XmlVersion,
	namespace::Namespace,
	writer::{
		XmlEvent,
		EventWriter,
		Error,
		EmitterConfig,
		events::StartElementBuilder,
	},
};


fn print_plain<W: Write>(w: &mut EventWriter<W>, elem: &str, value: &str)
						 -> Result<(), Error> {
	w.write(XmlEvent::start_element(elem))?;
	w.write(XmlEvent::characters(value))?;
	w.write(XmlEvent::end_element().name(elem))?;
	Ok(())
}

fn print_option<W: Write>(w: &mut EventWriter<W>, n: &str, v: &Option<String>)
					 -> Result<(), Error> {
	if let Some(x) = v { print_plain(w, n, x)? }
	Ok(())
}

fn print_vec<W: Write>(w: &mut EventWriter<W>, n: &str, v: &Vec<String>)
					   -> Result<(), Error> {
	for x in v {
		print_plain(w, n, x.as_str())?;
	}
	Ok(())
}

fn print_num<W: Write>(w: &mut EventWriter<W>, name: &str, v: &Option<u64>)
			 -> Result<(), Error> {
	if let Some(x) = v {
		// format! allocates a string, so we do this by hand
		// 30 characters should be more than enough to fit any integer up to
		// the 64 bit maximum.
		let mut buf = [0_u8; 30];
		let mut i = 0;
		let mut n = *x;
		loop {
			let d = (n % 10) as u8;
			n /= 10;
			i += 1;
			buf[buf.len() - i] = d + b'0';
			if n == 0 { break }
		}
		print_plain(w, name, std::str::from_utf8(&buf[buf.len()-i..])
					.expect("print_num generated invalid utf8!"))?;
	}
	Ok(())
}

fn print_meta<W: Write>(w: &mut EventWriter<W>,
						name: &str,
						ms: &Vec<Meta>)
			  -> Result<(), Error> {
	for m in ms {
		w.write(XmlEvent::start_element(name).attr("rel", &m.rel))?;
		w.write(XmlEvent::characters(&m.content))?;
		w.write(XmlEvent::end_element().name(name))?;
	}
	Ok(())
}

fn include_ns<'a>(elm: StartElementBuilder<'a>, ns: &Namespace)
				  -> StartElementBuilder<'a> {
	let mut bld = elm;
	for (prefix, uri) in ns {
		bld = bld.ns(prefix, uri);
	}
	bld
}

// write a single <extension> element
fn print_extension_elem<W: Write>(w: &mut EventWriter<W>, ext: &Extension)
							 -> Result<(), Error> {
	w.write(include_ns(
		XmlEvent::start_element("extension")
			.attr("application", &ext.application),
		&ext.namespace))?;
	for ev in &ext.content {
		if let Some(wev) = ev.as_writer_event() {
			w.write(wev)?;
		} else {
            // end of document event withing extension?
            // this is weird and should never happen,
            // but if it does happen, we should probably ignore it.
        }
	}
	w.write(XmlEvent::end_element().name("extension"))?;

	Ok(())
}

fn print_extension<W: Write>(w: &mut EventWriter<W>, exts: &Vec<Extension>)
							 -> Result<(), Error> {
	for ext in exts {
		print_extension_elem(w, ext)?;
	}
	Ok(())
}


fn print_playlist<W: Write>(w: &mut EventWriter<W>, p: &Playlist)
							-> Result<(), Error> {
	use XmlEvent::{EndElement, StartDocument};
	use print_option as o;
	use print_vec as v;
	use print_num as n;
	w.write(StartDocument{
		version: XmlVersion::Version10,
		encoding: Some("UTF-8"),
		standalone: None,
	})?;
	w.write(XmlEvent::start_element("playlist")
			.attr("version", "1")
			.default_ns("http://xspf.org/ns/0/"))?;
	{
		o(w, "title", &p.title)?;
		o(w, "creator", &p.creator)?;
		o(w, "annotation", &p.annotation)?;
		o(w, "info", &p.info)?;
		o(w, "location", &p.location)?;
		o(w, "identifier", &p.identifier)?;
		o(w, "image", &p.image)?;
		o(w, "date", &p.date)?;
		o(w, "license", &p.license)?;
		if !p.attribution.is_empty() {
			w.write(XmlEvent::start_element("attribution"))?;
			for attrib in &p.attribution {
				match attrib {
					Attribution::Location(s) => print_plain(w, "location", s)?,
					Attribution::Identifier(s) => print_plain(w, "identifier", s)?,
				}
			}
			w.write(XmlEvent::end_element().name("attribution"))?;
		}
		print_meta(w, "link", &p.link)?;
		print_meta(w, "meta", &p.meta)?;
		print_extension(w, &p.extension)?;
	}
	w.write(XmlEvent::start_element("trackList"))?;
	for t in &p.track_list {
		w.write(XmlEvent::start_element("track"))?;
		v(w, "location", &t.location)?;
		v(w, "identifier", &t.identifier)?;
		o(w, "title", &t.title)?;
		o(w, "creator", &t.creator)?;
		o(w, "annotation", &t.annotation)?;
		o(w, "info", &t.info)?;
		o(w, "image", &t.image)?;
		o(w, "album", &t.album)?;
		n(w, "trackNum", &t.track_num)?;
		n(w, "duration", &t.duration)?;
		print_meta(w, "link", &t.link)?;
		print_meta(w, "meta", &t.meta)?;
		print_extension(w, &t.extension)?;
		w.write(XmlEvent::end_element().name("track"))?;
	}
	w.write(XmlEvent::end_element().name("trackList"))?;
	w.write(EndElement{
		name: Some("playlist".into()),
	})?;
	Ok(())
}

fn default_cfg() -> EmitterConfig {
	// reduce memory allocations by always specifying the closing tag name manually
	EmitterConfig::default()
		.keep_element_names_stack(false)
		.pad_self_closing(false)
}

impl Playlist {
	/// write xml representation to the specified `io::Write` implementation
	pub fn write_to<W: Write>(&self, w: W) -> Result<(), Error> {
		// TODO: needs config
		print_playlist(&mut EventWriter::new(w), self)
	}

	pub fn write_to_pretty<W: Write, S: Into<Cow<'static, str>>>(&self, w: W, indent: S) -> Result<(), Error> {
		self.write_to_with_config(w, default_cfg()
								  .perform_indent(true)
								  .indent_string(indent))
		}

	fn write_to_with_config<W: Write>(&self, w: W, cfg: EmitterConfig)
							  -> Result<(), Error> {
		print_playlist(&mut EventWriter::new_with_config(w, cfg), self)
	}

	/// write xml representation to the specified file.
	/// the file will be created if it does not already exist.
	pub fn write_file<P: AsRef<Path>>(&self, filepath: P) -> Result<(), Error> {
		let f = File::create(filepath)?;
		self.write_to(f)
	}

	/// serialize the Playlist to an xml string.
	#[must_use] pub fn to_string(&self) -> String {
		self.to_string_with_config(default_cfg())
	}

	// left unexported to prevent the user from disabling cfg.perform_escaping
	fn to_string_with_config(&self, cfg: EmitterConfig) -> String {
		let mut v: Vec<u8> = vec![];
		// since we are writing to a Vec, we should never encounter an io error
		self.write_to_with_config(&mut v, cfg).unwrap();
		String::from_utf8(v).unwrap()		
	}

	/// same as `to_string`, but with newlines and indentation
	pub fn to_string_pretty<S: Into<Cow<'static, str>>>(&self, indent: S) -> String {
		self.to_string_with_config(default_cfg()
				.perform_indent(true)
				.indent_string(indent))
	}
}

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

	#[test]
	fn print_simple() {
		let track1 = Track{
			title: Some("Track1".to_string()),
			track_num: Some(1),
			duration: Some(0),
			location: vec!["file:///dev/null".to_string()],
			.. Track::default() };
		let mut p1 = Playlist::default();
		p1.identifier = Some("foo:bar".to_string());
		p1.accredit();
		p1.identifier = Some("foo:newbar".to_string());
		p1.title = Some("sometitle".to_string());
		p1.creator =
			Some("Cool Person <coolperson@email.example.org>".to_string());
		p1.track_list.push(track1);
		p1.meta.push(Meta{
			rel: "http://example.org/ns/1".to_string(),
			content: "foobar".to_string()
		});
		
		assert_eq!(
			p1.to_string_pretty("  "),
			r#"<?xml version="1.0" encoding="UTF-8"?>
<playlist xmlns="http://xspf.org/ns/0/" version="1">
  <title>sometitle</title>
  <creator>Cool Person &lt;coolperson@email.example.org&gt;</creator>
  <identifier>foo:newbar</identifier>
  <attribution>
    <identifier>foo:bar</identifier>
  </attribution>
  <meta rel="http://example.org/ns/1">foobar</meta>
  <trackList>
    <track>
      <location>file:///dev/null</location>
      <title>Track1</title>
      <trackNum>1</trackNum>
      <duration>0</duration>
    </track>
  </trackList>
</playlist>"#);
	}

	#[test]
	fn do_roundtrips() {
		let s1 = r#"<?xml version="1.0" encoding="UTF-8"?>
<playlist xmlns="http://xspf.org/ns/0/" version="1">
        <title>Playlist</title>
        <extension xmlns:vlc="http://www.videolan.org/vlc/playlist/ns/0/" application="http://www.videolan.org/vlc/playlist/0">
                <vlc:item tid="0"/>
                <vlc:item tid="1"/>
        </extension>
        <trackList>
                <track>
                        <location>https://upload.wikimedia.org/wikipedia/commons/6/60/Bach_-_cantata_140._1._chorus.ogg</location>
                        <title>Cantata 140. 1. Chorus</title>
                        <creator>MIT Concert Choir (Cutter)</creator>
                        <album>Bach and Schubert</album>
                        <trackNum>8</trackNum>
                        <duration>347324</duration>
                        <extension xmlns:vlc="http://www.videolan.org/vlc/playlist/ns/0/" application="http://www.videolan.org/vlc/playlist/0">
                                <vlc:id>0</vlc:id>
                                <vlc:option>network-caching=1000</vlc:option>
                        </extension>
                </track>
                <track>
                        <location>https://upload.wikimedia.org/wikipedia/commons/8/87/Beethoven_-_opus47-1_01.ogg</location>
                        <title>Beethoven:violin-piano sonatas</title>
                        <creator>Paul Rosenthal and Edward Auer</creator>
                        <duration>623046</duration>
                        <extension xmlns:vlc="http://www.videolan.org/vlc/playlist/ns/0/" application="http://www.videolan.org/vlc/playlist/0">
                                <vlc:id>1</vlc:id>
                                <vlc:option>network-caching=1000</vlc:option>
                        </extension>
                </track>
        </trackList>
</playlist>"#;
		let p1: Playlist = s1.parse().unwrap();
		let r1 = p1.to_string_pretty("        ");
		if r1 != s1 {
			println!("=== original ===\n{s1}");
			println!("=== modified ===\n{r1}");
			assert!(false);
		}
	}
}