use std::fmt;
#[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, ",")?;
}
if v.contains([',', ':', ';', '"']) || v.contains(char::is_whitespace) {
write!(f, "\"{}\"", v)?;
} else {
write!(f, "{}", v)?;
}
}
Ok(())
}
}
#[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
}
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)
}
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");
}
}