ezcal 0.3.4

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

/// A parameter on a content line (e.g., `LANGUAGE=en` or `VALUE=DATE`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Parameter {
    pub name: String,
    pub values: Vec<String>,
}

impl Parameter {
    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            values: vec![value.into()],
        }
    }

    pub fn with_values(name: impl Into<String>, values: Vec<String>) -> Self {
        Self {
            name: name.into(),
            values,
        }
    }
}

impl fmt::Display for Parameter {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.name)?;
        write!(f, "=")?;
        for (i, v) in self.values.iter().enumerate() {
            if i > 0 {
                write!(f, ",")?;
            }
            // Quote values that contain special characters
            if v.contains([',', ':', ';', '"']) || v.contains(char::is_whitespace) {
                write!(f, "\"{}\"", v)?;
            } else {
                write!(f, "{}", v)?;
            }
        }
        Ok(())
    }
}

/// A property in an iCalendar or vCard object (e.g., `SUMMARY:Team Standup`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Property {
    pub name: String,
    pub params: Vec<Parameter>,
    pub value: String,
}

impl Property {
    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            params: Vec::new(),
            value: value.into(),
        }
    }

    pub fn with_param(mut self, param: Parameter) -> Self {
        self.params.push(param);
        self
    }

    pub fn with_params(mut self, params: Vec<Parameter>) -> Self {
        self.params = params;
        self
    }

    /// Get the first parameter with the given name.
    pub fn param(&self, name: &str) -> Option<&Parameter> {
        let name_upper = name.to_uppercase();
        self.params
            .iter()
            .find(|p| p.name.to_uppercase() == name_upper)
    }

    /// Get the first value of a parameter with the given name.
    pub fn param_value(&self, name: &str) -> Option<&str> {
        self.param(name)
            .and_then(|p| p.values.first())
            .map(|s| s.as_str())
    }
}

impl fmt::Display for Property {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.name)?;
        for param in &self.params {
            write!(f, ";{}", param)?;
        }
        write!(f, ":{}", self.value)?;
        Ok(())
    }
}

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

    #[test]
    fn property_display_simple() {
        let prop = Property::new("SUMMARY", "Team Standup");
        assert_eq!(prop.to_string(), "SUMMARY:Team Standup");
    }

    #[test]
    fn property_display_with_params() {
        let prop = Property::new("DTSTART", "20260315T090000")
            .with_param(Parameter::new("VALUE", "DATE-TIME"))
            .with_param(Parameter::new("TZID", "America/New_York"));
        assert_eq!(
            prop.to_string(),
            "DTSTART;VALUE=DATE-TIME;TZID=America/New_York:20260315T090000"
        );
    }

    #[test]
    fn parameter_quoted_value() {
        let param = Parameter::new("CN", "John Doe");
        assert_eq!(param.to_string(), "CN=\"John Doe\"");
    }

    #[test]
    fn parameter_multi_value() {
        let param = Parameter::with_values("TYPE", vec!["WORK".into(), "VOICE".into()]);
        assert_eq!(param.to_string(), "TYPE=WORK,VOICE");
    }
}