xmltv 2.1.0

XMLTV for electronic program guide (EPG) parser and generator using serde.
Documentation
use std::fmt;

use serde::{
    de::{self, MapAccess, SeqAccess, Visitor},
    Deserialize, Deserializer, Serializer,
};

use crate::{Channel, ValueAndLang};

pub fn bool_to_new_tag<S>(x: &bool, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    if *x {
        s.serialize_some("")
    } else {
        s.serialize_none()
    }
}

/// When present always return true
pub fn new_tag_to_boolean<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
    D: de::Deserializer<'de>,
{
    let _: () = de::Deserialize::deserialize(deserializer)?;
    Ok(true)
}

pub fn bool_to_yes_no<S>(x: &bool, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    if *x {
        s.serialize_str("yes")
    } else {
        s.serialize_str("no")
    }
}

pub fn yes_no_to_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
    D: de::Deserializer<'de>,
{
    let value: Option<&str> = de::Deserialize::deserialize(deserializer)?;
    if let Some(v) = value {
        return Ok(v.to_lowercase() == "yes");
    }
    Ok(false)
}

impl<'de> Deserialize<'de> for ValueAndLang {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Debug)]
        enum Field {
            Lang,
            Text,
        }
        impl<'de> Deserialize<'de> for Field {
            fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
            where
                D: Deserializer<'de>,
            {
                struct FieldVisitor;

                impl Visitor<'_> for FieldVisitor {
                    type Value = Field;

                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                        formatter.write_str("`$text` or `@lang`")
                    }

                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
                    where
                        E: de::Error,
                    {
                        match value {
                            "@lang" => Ok(Field::Lang),
                            "$text" => Ok(Field::Text),
                            _ => Err(de::Error::unknown_field(value, FIELDS)),
                        }
                    }
                }

                deserializer.deserialize_identifier(FieldVisitor)
            }
        }

        struct ValueAndLangVisitor;

        impl<'de> Visitor<'de> for ValueAndLangVisitor {
            type Value = ValueAndLang;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("struct ValueAndLang")
            }

            fn visit_seq<V>(self, mut seq: V) -> Result<ValueAndLang, V::Error>
            where
                V: SeqAccess<'de>,
            {
                let text = seq
                    .next_element()?
                    .ok_or_else(|| de::Error::invalid_length(0, &self))?;
                let lang = seq
                    .next_element()?
                    .ok_or_else(|| de::Error::invalid_length(1, &self))?;
                Ok(ValueAndLang { value: text, lang })
            }

            fn visit_map<V>(self, mut map: V) -> Result<ValueAndLang, V::Error>
            where
                V: MapAccess<'de>,
            {
                let mut text = None;
                let mut lang = None;
                while let Some(key) = map.next_key()? {
                    // println!("key: {:?}", key);
                    match key {
                        Field::Text => {
                            if text.is_some() {
                                // println!("$text: {:?}", text);
                                return Err(de::Error::duplicate_field("text"));
                            }
                            text = Some(map.next_value()?);
                        }
                        Field::Lang => {
                            if lang.is_some() {
                                // println!("@lang: {:?}", lang);
                                return Err(de::Error::duplicate_field("lang"));
                            }
                            lang = Some(map.next_value()?);
                        }
                    }
                }
                // println!("\t$text: {:?}", text);
                // println!("\t@lang: {:?}", lang);
                let text = text.unwrap_or(String::new());
                let lang = lang.unwrap_or(None);
                Ok(ValueAndLang { value: text, lang })
            }
        }

        const FIELDS: &[&str] = &["$text", "@lang"];
        deserializer.deserialize_struct("ValueAndLang", FIELDS, ValueAndLangVisitor)
    }
}

#[allow(dead_code)]
trait Trimable {
    fn trimmed(self) -> String;
}

impl Trimable for String {
    /// Remove whitespaces from CDATA section.
    /// - in the beginning and in the end of each line
    /// - if the line does not ends with `.` or `!` or `!` a space is added to continue the sentence
    fn trimmed(self) -> String {
        // remove beginning spaces
        let value: String = self.trim_start_matches(char::is_whitespace).to_owned();
        // trim middle lines
        let data = value.lines();
        let n = data.count();
        if n == 0 {
            return String::new();
        } else if n == 1 {
            return value.trim().to_owned();
        }
        let mut result = String::with_capacity(value.len());
        for (i, line) in value.lines().enumerate() {
            let trimmed = line.trim_matches(char::is_whitespace);
            result.push_str(trimmed);
            let ends_with_punctuation = trimmed.ends_with(['.', '!', '?']);
            if !ends_with_punctuation {
                result.push(' ');
            }
            if i < n - 1 && ends_with_punctuation {
                result.push('\n');
            }
        }
        // remove end spaces
        let new_len = result
            .char_indices()
            .rev()
            .find(|(_, c)| !matches!(c, '\n' | '\r' | ' ' | '\t'))
            .map_or(0, |(i, _)| i + 1);
        if new_len != self.len() {
            result.truncate(new_len);
        }
        result
    }
}

pub fn channels_to_map(channels: &[Channel]) -> std::collections::HashMap<&str, Channel> {
    let mut map: std::collections::HashMap<&str, Channel> =
        std::collections::HashMap::with_capacity(channels.len());
    for c in channels {
        map.insert(c.id.as_str(), c.clone());
    }
    map
}

#[cfg(test)]
mod tests {
    use crate::utils::Trimable;

    #[test]
    fn test_trim_strings() {
        assert_eq!(String::new().trimmed(), "".to_owned());
        assert_eq!(String::from("\n\n").trimmed(), "".to_owned());
        assert_eq!(String::from("a\n \t\n").trimmed(), "a".to_owned());
        assert_eq!(String::from("  \t a\t   ").trimmed(), "a".to_owned());
        assert_eq!(String::from("a\na\n\n").trimmed(), "a a".to_owned());
        assert_eq!(String::from("\n\na\na\n\n").trimmed(), "a a".to_owned());
        assert_eq!(String::from("\n\na.\na\n\n").trimmed(), "a.\na".to_owned());
        assert_eq!(String::from("a.\n").trimmed(), "a.".to_owned());
        assert_eq!(String::from("Actresses Kim Raver, Brooke Shields and Lindsay Price (\"Lipstick Jungle\");
        women in their 40s tell why they got breast implants; a 30-minute meal.").trimmed(), "Actresses Kim Raver, Brooke Shields and Lindsay Price (\"Lipstick Jungle\"); women in their 40s tell why they got breast implants; a 30-minute meal.".to_owned());
        assert_eq!(String::from("      Bilko claims he's had a close encounter with an alien in order
        to be given some compassionate leave so he can visit an old
        flame in New York.").trimmed(), "Bilko claims he's had a close encounter with an alien in order to be given some compassionate leave so he can visit an old flame in New York.".to_owned());
    }
}