ezcal 0.3.4

Ergonomic iCalendar + vCard library for Rust
Documentation
use std::fmt;

/// A structured name (N property) per RFC 6350 ยง6.2.2.
///
/// Components: family name, given name, additional names, honorific prefixes, honorific suffixes.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StructuredName {
    pub family: String,
    pub given: String,
    pub additional: String,
    pub prefix: String,
    pub suffix: String,
}

impl StructuredName {
    pub fn new(family: impl Into<String>, given: impl Into<String>) -> Self {
        Self {
            family: family.into(),
            given: given.into(),
            additional: String::new(),
            prefix: String::new(),
            suffix: String::new(),
        }
    }

    pub fn with_additional(mut self, additional: impl Into<String>) -> Self {
        self.additional = additional.into();
        self
    }

    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
        self.prefix = prefix.into();
        self
    }

    pub fn with_suffix(mut self, suffix: impl Into<String>) -> Self {
        self.suffix = suffix.into();
        self
    }

    /// Parse from the value of an N property (semicolon-separated).
    pub fn parse(s: &str) -> Self {
        let parts = split_semicolons(s, 5);
        Self {
            family: parts.first().map(|s| unescape(s)).unwrap_or_default(),
            given: parts.get(1).map(|s| unescape(s)).unwrap_or_default(),
            additional: parts.get(2).map(|s| unescape(s)).unwrap_or_default(),
            prefix: parts.get(3).map(|s| unescape(s)).unwrap_or_default(),
            suffix: parts.get(4).map(|s| unescape(s)).unwrap_or_default(),
        }
    }

    /// Serialize to the value format for an N property.
    pub fn to_value(&self) -> String {
        format!(
            "{};{};{};{};{}",
            escape(&self.family),
            escape(&self.given),
            escape(&self.additional),
            escape(&self.prefix),
            escape(&self.suffix)
        )
    }
}

impl fmt::Display for StructuredName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.to_value())
    }
}

/// A structured address (ADR property) per RFC 6350 ยง6.3.1.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Address {
    pub po_box: String,
    pub extended: String,
    pub street: String,
    pub city: String,
    pub region: String,
    pub postal_code: String,
    pub country: String,
}

impl Address {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn street(mut self, street: impl Into<String>) -> Self {
        self.street = street.into();
        self
    }

    pub fn city(mut self, city: impl Into<String>) -> Self {
        self.city = city.into();
        self
    }

    pub fn region(mut self, region: impl Into<String>) -> Self {
        self.region = region.into();
        self
    }

    pub fn postal_code(mut self, postal_code: impl Into<String>) -> Self {
        self.postal_code = postal_code.into();
        self
    }

    pub fn country(mut self, country: impl Into<String>) -> Self {
        self.country = country.into();
        self
    }

    pub fn po_box(mut self, po_box: impl Into<String>) -> Self {
        self.po_box = po_box.into();
        self
    }

    pub fn extended(mut self, extended: impl Into<String>) -> Self {
        self.extended = extended.into();
        self
    }

    /// Parse from the value of an ADR property (semicolon-separated).
    pub fn parse(s: &str) -> Self {
        let parts = split_semicolons(s, 7);
        Self {
            po_box: parts.first().map(|s| unescape(s)).unwrap_or_default(),
            extended: parts.get(1).map(|s| unescape(s)).unwrap_or_default(),
            street: parts.get(2).map(|s| unescape(s)).unwrap_or_default(),
            city: parts.get(3).map(|s| unescape(s)).unwrap_or_default(),
            region: parts.get(4).map(|s| unescape(s)).unwrap_or_default(),
            postal_code: parts.get(5).map(|s| unescape(s)).unwrap_or_default(),
            country: parts.get(6).map(|s| unescape(s)).unwrap_or_default(),
        }
    }

    /// Serialize to the value format for an ADR property.
    pub fn to_value(&self) -> String {
        format!(
            "{};{};{};{};{};{};{}",
            escape(&self.po_box),
            escape(&self.extended),
            escape(&self.street),
            escape(&self.city),
            escape(&self.region),
            escape(&self.postal_code),
            escape(&self.country)
        )
    }
}

impl fmt::Display for Address {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.to_value())
    }
}

/// Split on semicolons, respecting backslash-escaped semicolons.
fn split_semicolons(s: &str, max: usize) -> Vec<String> {
    let mut parts = Vec::new();
    let mut current = String::new();
    let mut chars = s.chars().peekable();

    while let Some(ch) = chars.next() {
        if ch == '\\' {
            current.push(ch);
            if let Some(&next) = chars.peek() {
                current.push(next);
                chars.next();
            }
        } else if ch == ';' && parts.len() + 1 < max {
            parts.push(current);
            current = String::new();
        } else {
            current.push(ch);
        }
    }
    parts.push(current);
    parts
}

fn escape(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace(';', "\\;")
        .replace(',', "\\,")
        .replace('\n', "\\n")
}

fn unescape(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut chars = s.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '\\' {
            match chars.peek() {
                Some('n') | Some('N') => {
                    result.push('\n');
                    chars.next();
                }
                Some('\\') => {
                    result.push('\\');
                    chars.next();
                }
                Some(';') => {
                    result.push(';');
                    chars.next();
                }
                Some(',') => {
                    result.push(',');
                    chars.next();
                }
                _ => result.push('\\'),
            }
        } else {
            result.push(ch);
        }
    }
    result
}

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

    #[test]
    fn name_roundtrip() {
        let name = StructuredName::new("Doe", "Jane")
            .with_additional("Marie")
            .with_prefix("Dr.")
            .with_suffix("PhD");
        let value = name.to_value();
        let parsed = StructuredName::parse(&value);
        assert_eq!(name, parsed);
    }

    #[test]
    fn address_roundtrip() {
        let addr = Address::new()
            .street("123 Main St")
            .city("Anytown")
            .region("CA")
            .postal_code("90210")
            .country("USA");
        let value = addr.to_value();
        let parsed = Address::parse(&value);
        assert_eq!(addr, parsed);
    }

    #[test]
    fn name_with_semicolons() {
        let name = StructuredName::new("O;Brien", "Conan");
        let value = name.to_value();
        let parsed = StructuredName::parse(&value);
        assert_eq!(parsed.family, "O;Brien");
        assert_eq!(parsed.given, "Conan");
    }
}