use std::io::Write;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
use crate::{Channel, Programme, Tv};
#[derive(Debug)]
pub enum WriteError {
Serialize(String),
Io(std::io::Error),
}
impl std::fmt::Display for WriteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WriteError::Serialize(msg) => write!(f, "serialization error: {msg}"),
WriteError::Io(e) => write!(f, "I/O error: {e}"),
}
}
}
impl std::error::Error for WriteError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
WriteError::Io(e) => Some(e),
WriteError::Serialize(_) => None,
}
}
}
impl From<std::io::Error> for WriteError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl From<quick_xml::SeError> for WriteError {
fn from(e: quick_xml::SeError) -> Self {
Self::Serialize(e.to_string())
}
}
impl From<quick_xml::Error> for WriteError {
fn from(e: quick_xml::Error) -> Self {
Self::Serialize(e.to_string())
}
}
pub struct TvWriter<W: Write> {
inner: W,
}
impl<W: Write> TvWriter<W> {
pub fn new(mut inner: W, tv: &Tv) -> Result<Self, WriteError> {
{
let mut w = quick_xml::Writer::new(&mut inner);
w.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
let mut elem = BytesStart::new("tv");
if let Some(ref v) = tv.source_info_url {
elem.push_attribute(("source-info-url", v.as_str()));
}
if let Some(ref v) = tv.source_info_name {
elem.push_attribute(("source-info-name", v.as_str()));
}
if let Some(ref v) = tv.source_data_url {
elem.push_attribute(("source-data-url", v.as_str()));
}
if let Some(ref v) = tv.generator_info_name {
elem.push_attribute(("generator-info-name", v.as_str()));
}
if let Some(ref v) = tv.generator_info_url {
elem.push_attribute(("generator-info-url", v.as_str()));
}
w.write_event(Event::Start(elem))?;
}
Ok(Self { inner })
}
pub fn write_channel(&mut self, channel: &Channel) -> Result<(), WriteError> {
let xml = quick_xml::se::to_string_with_root("channel", channel)?;
self.inner.write_all(xml.as_bytes())?;
Ok(())
}
pub fn write_programme(&mut self, programme: &Programme) -> Result<(), WriteError> {
let xml = quick_xml::se::to_string_with_root("programme", programme)?;
self.inner.write_all(xml.as_bytes())?;
Ok(())
}
pub fn finish(mut self) -> Result<W, WriteError> {
{
let mut w = quick_xml::Writer::new(&mut self.inner);
w.write_event(Event::End(BytesEnd::new("tv")))?;
}
Ok(self.inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{NameAndLang, ValueAndLang};
fn write_empty(tv: &Tv) -> String {
let mut buf: Vec<u8> = Vec::new();
let w = TvWriter::new(&mut buf, tv).unwrap();
w.finish().unwrap();
String::from_utf8(buf).unwrap()
}
#[test]
fn test_tv_writer_empty_contains_declaration_and_tags() {
let xml = write_empty(&Tv::default());
assert!(xml.contains("<?xml"), "missing declaration: {xml}");
assert!(
xml.contains("<tv>") || xml.contains("<tv "),
"missing <tv>: {xml}"
);
assert!(xml.contains("</tv>"), "missing </tv>: {xml}");
}
#[test]
fn test_tv_writer_attributes_written() {
let tv = Tv {
generator_info_name: Some("test/1.0".into()),
source_info_name: Some("My Source".into()),
..Default::default()
};
let xml = write_empty(&tv);
assert!(
xml.contains("generator-info-name=\"test/1.0\""),
"xml: {xml}"
);
assert!(xml.contains("source-info-name=\"My Source\""), "xml: {xml}");
}
#[test]
fn test_tv_writer_write_channel() {
let mut buf: Vec<u8> = Vec::new();
let mut w = TvWriter::new(&mut buf, &Tv::default()).unwrap();
w.write_channel(&Channel {
id: "bbc1.uk".into(),
display_names: vec![NameAndLang {
name: "BBC One".into(),
lang: None,
}],
..Default::default()
})
.unwrap();
w.finish().unwrap();
let xml = String::from_utf8(buf).unwrap();
assert!(xml.contains("<channel"), "xml: {xml}");
assert!(xml.contains("bbc1.uk"), "xml: {xml}");
assert!(xml.contains("BBC One"), "xml: {xml}");
}
#[test]
fn test_tv_writer_write_programme() {
let mut buf: Vec<u8> = Vec::new();
let mut w = TvWriter::new(&mut buf, &Tv::default()).unwrap();
w.write_programme(&Programme {
channel: "arte.tv".into(),
start: "20240101200000 +0000".into(),
titles: vec![ValueAndLang {
value: "Le Journal".into(),
lang: Some("fr".into()),
}],
..Default::default()
})
.unwrap();
w.finish().unwrap();
let xml = String::from_utf8(buf).unwrap();
assert!(xml.contains("<programme"), "xml: {xml}");
assert!(xml.contains("arte.tv"), "xml: {xml}");
assert!(xml.contains("Le Journal"), "xml: {xml}");
}
#[test]
fn test_tv_writer_output_is_valid_xml() {
let mut buf: Vec<u8> = Vec::new();
let mut w = TvWriter::new(&mut buf, &Tv::default()).unwrap();
w.write_channel(&Channel {
id: "ch1".into(),
..Default::default()
})
.unwrap();
w.write_programme(&Programme {
channel: "ch1".into(),
start: "20240101120000 +0000".into(),
..Default::default()
})
.unwrap();
w.finish().unwrap();
let xml = String::from_utf8(buf).unwrap();
let mut reader = quick_xml::Reader::from_str(&xml);
let mut buf2 = Vec::new();
loop {
match reader.read_event_into(&mut buf2) {
Ok(quick_xml::events::Event::Eof) => break,
Err(e) => panic!("XML parse error: {e}"),
_ => {}
}
buf2.clear();
}
}
#[test]
fn test_tv_writer_multiple_channels_and_programmes() {
let mut buf: Vec<u8> = Vec::new();
let mut w = TvWriter::new(&mut buf, &Tv::default()).unwrap();
for i in 1..=3 {
w.write_channel(&Channel {
id: format!("ch{i}"),
..Default::default()
})
.unwrap();
}
for i in 1..=5 {
w.write_programme(&Programme {
channel: format!("ch{}", (i % 3) + 1),
start: format!("202401011{i}0000 +0000"),
..Default::default()
})
.unwrap();
}
w.finish().unwrap();
let xml = String::from_utf8(buf).unwrap();
assert_eq!(xml.matches("<channel").count(), 3);
assert_eq!(xml.matches("<programme").count(), 5);
}
}