use super::{Parameter, Property};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CUType {
Individual,
Group,
Resource,
Room,
Unknown,
}
impl CUType {
pub(crate) fn from_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"INDIVIDUAL" => Some(CUType::Individual),
"GROUP" => Some(CUType::Group),
"RESOURCE" => Some(CUType::Resource),
"ROOM" => Some(CUType::Room),
"UNKNOWN" => Some(CUType::Unknown),
_ => None,
}
}
}
impl From<CUType> for Parameter {
fn from(cutype: CUType) -> Self {
Parameter::new(
"CUTYPE",
match cutype {
CUType::Individual => "INDIVIDUAL",
CUType::Group => "GROUP",
CUType::Resource => "RESOURCE",
CUType::Room => "ROOM",
CUType::Unknown => "UNKNOWN",
},
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Role {
Chair,
ReqParticipant,
OptParticipant,
NonParticipant,
}
impl Role {
pub(crate) fn from_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"CHAIR" => Some(Role::Chair),
"REQ-PARTICIPANT" => Some(Role::ReqParticipant),
"OPT-PARTICIPANT" => Some(Role::OptParticipant),
"NON-PARTICIPANT" => Some(Role::NonParticipant),
_ => None,
}
}
}
impl From<Role> for Parameter {
fn from(role: Role) -> Self {
Parameter::new(
"ROLE",
match role {
Role::Chair => "CHAIR",
Role::ReqParticipant => "REQ-PARTICIPANT",
Role::OptParticipant => "OPT-PARTICIPANT",
Role::NonParticipant => "NON-PARTICIPANT",
},
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PartStat {
NeedsAction,
Accepted,
Declined,
Tentative,
Delegated,
Completed,
InProcess,
}
impl PartStat {
pub(crate) fn from_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"NEEDS-ACTION" => Some(PartStat::NeedsAction),
"ACCEPTED" => Some(PartStat::Accepted),
"DECLINED" => Some(PartStat::Declined),
"TENTATIVE" => Some(PartStat::Tentative),
"DELEGATED" => Some(PartStat::Delegated),
"COMPLETED" => Some(PartStat::Completed),
"IN-PROCESS" => Some(PartStat::InProcess),
_ => None,
}
}
}
impl From<PartStat> for Parameter {
fn from(partstat: PartStat) -> Self {
Parameter::new(
"PARTSTAT",
match partstat {
PartStat::NeedsAction => "NEEDS-ACTION",
PartStat::Accepted => "ACCEPTED",
PartStat::Declined => "DECLINED",
PartStat::Tentative => "TENTATIVE",
PartStat::Delegated => "DELEGATED",
PartStat::Completed => "COMPLETED",
PartStat::InProcess => "IN-PROCESS",
},
)
}
}
fn encode_cal_address_list(addrs: &[String]) -> String {
addrs
.iter()
.map(|a| format!("\"{}\"", a))
.collect::<Vec<_>>()
.join(",")
}
fn decode_cal_address_list(s: &str) -> Vec<String> {
s.split(',')
.map(|part| {
let trimmed = part.trim();
if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
trimmed[1..trimmed.len() - 1].to_string()
} else {
trimmed.to_string()
}
})
.filter(|s| !s.is_empty())
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Attendee {
pub cn: Option<String>,
pub cutype: Option<CUType>,
pub delegated_from: Vec<String>,
pub delegated_to: Vec<String>,
pub dir: Option<String>,
pub language: Option<String>,
pub member: Vec<String>,
pub part_stat: Option<PartStat>,
pub role: Option<Role>,
pub rsvp: Option<bool>,
pub sent_by: Option<String>,
pub cal_address: String,
}
impl Attendee {
pub fn new(cal_address: String) -> Self {
Self {
cal_address,
..Default::default()
}
}
pub fn cutype(mut self, cutype: CUType) -> Self {
self.cutype = Some(cutype);
self
}
pub fn member(mut self, member: String) -> Self {
self.member.push(member);
self
}
pub fn role(mut self, role: Role) -> Self {
self.role = Some(role);
self
}
pub fn partstat(mut self, partstat: PartStat) -> Self {
self.part_stat = Some(partstat);
self
}
pub fn rsvp(mut self, rsvp: bool) -> Self {
self.rsvp = Some(rsvp);
self
}
pub fn delegated_to(mut self, delegated_to: String) -> Self {
self.delegated_to.push(delegated_to);
self
}
pub fn delegated_from(mut self, delegated_from: String) -> Self {
self.delegated_from.push(delegated_from);
self
}
pub fn sentby(mut self, sentby: String) -> Self {
self.sent_by = Some(sentby);
self
}
pub fn cn(mut self, cn: String) -> Self {
self.cn = Some(cn);
self
}
pub fn dir(mut self, dir: String) -> Self {
self.dir = Some(dir);
self
}
pub fn language(mut self, language: String) -> Self {
self.language = Some(language);
self
}
}
impl From<Attendee> for Property {
fn from(attendee: Attendee) -> Self {
let mut prop = Property::new("ATTENDEE", attendee.cal_address);
if let Some(cutype) = attendee.cutype {
prop.append_parameter(cutype);
}
if !attendee.member.is_empty() {
prop.add_parameter("MEMBER", &encode_cal_address_list(&attendee.member));
}
if let Some(role) = attendee.role {
prop.append_parameter(role);
}
if let Some(partstat) = attendee.part_stat {
prop.append_parameter(partstat);
}
if let Some(rsvp) = attendee.rsvp {
prop.add_parameter("RSVP", if rsvp { "TRUE" } else { "FALSE" });
}
if !attendee.delegated_to.is_empty() {
prop.add_parameter(
"DELEGATED-TO",
&encode_cal_address_list(&attendee.delegated_to),
);
}
if !attendee.delegated_from.is_empty() {
prop.add_parameter(
"DELEGATED-FROM",
&encode_cal_address_list(&attendee.delegated_from),
);
}
if let Some(sentby) = attendee.sent_by {
prop.add_parameter("SENT-BY", &sentby);
}
if let Some(cn) = attendee.cn {
prop.add_parameter("CN", &cn);
}
if let Some(dir) = attendee.dir {
prop.add_parameter("DIR", &dir);
}
if let Some(language) = attendee.language {
prop.add_parameter("LANGUAGE", &language);
}
prop.done()
}
}
impl TryFrom<&Property> for Attendee {
type Error = ();
fn try_from(prop: &Property) -> Result<Self, Self::Error> {
if prop.key() != "ATTENDEE" {
return Err(());
}
let value = prop.value().to_string();
let cutype = prop.get_param_as("CUTYPE", CUType::from_str);
let member = prop
.get_param_as("MEMBER", |s| Some(decode_cal_address_list(s)))
.unwrap_or_default();
let role = prop.get_param_as("ROLE", Role::from_str);
let partstat = prop.get_param_as("PARTSTAT", PartStat::from_str);
let rsvp = prop.get_param_as("RSVP", |s| match s.to_uppercase().as_str() {
"TRUE" => Some(true),
"FALSE" => Some(false),
_ => None,
});
let delegated_to = prop
.get_param_as("DELEGATED-TO", |s| Some(decode_cal_address_list(s)))
.unwrap_or_default();
let delegated_from = prop
.get_param_as("DELEGATED-FROM", |s| Some(decode_cal_address_list(s)))
.unwrap_or_default();
let sentby = prop.get_param_as("SENT-BY", |s| Some(s.to_string()));
let cn = prop.get_param_as("CN", |s| Some(s.to_string()));
let dir = prop.get_param_as("DIR", |s| Some(s.to_string()));
let language = prop.get_param_as("LANGUAGE", |s| Some(s.to_string()));
Ok(Attendee {
cal_address: value,
cutype,
member,
role,
part_stat: partstat,
rsvp,
delegated_to,
delegated_from,
sent_by: sentby,
cn,
dir,
language,
})
}
}
#[cfg(test)]
mod test_attendee {
use super::*;
#[test]
fn to_property_basic() {
let attendee = Attendee::new("mailto:test@example.com".to_string());
let prop: Property = attendee.into();
assert_eq!(prop.key(), "ATTENDEE");
assert_eq!(prop.value(), "mailto:test@example.com");
assert!(prop.params().is_empty());
}
#[test]
fn to_property_full() {
let attendee = Attendee::new("mailto:test@example.com".to_string())
.cutype(CUType::Individual)
.role(Role::ReqParticipant)
.partstat(PartStat::Accepted)
.rsvp(true)
.cn("Test User".to_string())
.member("mailto:member1@example.com".to_string())
.member("mailto:member2@example.com".to_string())
.delegated_to("mailto:delegate@example.com".to_string())
.sentby("mailto:sender@example.com".to_string())
.dir("ldap://example.com/cn=Test%20User".to_string())
.language("en".to_string());
let prop: Property = attendee.into();
assert_eq!(prop.key(), "ATTENDEE");
assert_eq!(prop.value(), "mailto:test@example.com");
assert_eq!(prop.params().get("CUTYPE").unwrap().value(), "INDIVIDUAL");
assert_eq!(
prop.params().get("ROLE").unwrap().value(),
"REQ-PARTICIPANT"
);
assert_eq!(prop.params().get("PARTSTAT").unwrap().value(), "ACCEPTED");
assert_eq!(prop.params().get("RSVP").unwrap().value(), "TRUE");
assert_eq!(prop.params().get("CN").unwrap().value(), "Test User");
assert_eq!(
prop.params().get("MEMBER").unwrap().value(),
"\"mailto:member1@example.com\",\"mailto:member2@example.com\""
);
assert_eq!(
prop.params().get("DELEGATED-TO").unwrap().value(),
"\"mailto:delegate@example.com\""
);
assert_eq!(
prop.params().get("SENT-BY").unwrap().value(),
"mailto:sender@example.com"
);
assert_eq!(
prop.params().get("DIR").unwrap().value(),
"ldap://example.com/cn=Test%20User"
);
assert_eq!(prop.params().get("LANGUAGE").unwrap().value(), "en");
}
#[test]
fn from_property_basic() {
let prop = Property::new("ATTENDEE", "mailto:test@example.com").done();
let attendee = Attendee::try_from(&prop).unwrap();
assert_eq!(attendee.cal_address, "mailto:test@example.com");
assert!(attendee.cutype.is_none());
assert!(attendee.role.is_none());
assert!(attendee.part_stat.is_none());
assert!(attendee.rsvp.is_none());
assert!(attendee.cn.is_none());
assert!(attendee.member.is_empty());
assert!(attendee.delegated_to.is_empty());
assert!(attendee.sent_by.is_none());
assert!(attendee.dir.is_none());
assert!(attendee.language.is_none());
}
#[test]
fn from_property_full() {
let prop = Property::new("ATTENDEE", "mailto:test@example.com")
.add_parameter("CUTYPE", "INDIVIDUAL")
.add_parameter("ROLE", "REQ-PARTICIPANT")
.add_parameter("PARTSTAT", "ACCEPTED")
.add_parameter("RSVP", "TRUE")
.add_parameter("CN", "Test User")
.add_parameter(
"MEMBER",
"\"mailto:member1@example.com\",\"mailto:member2@example.com\"",
)
.add_parameter("DELEGATED-TO", "\"mailto:delegate@example.com\"")
.add_parameter("DELEGATED-FROM", "\"mailto:delegator@example.com\"")
.add_parameter("SENT-BY", "mailto:sender@example.com")
.add_parameter("DIR", "ldap://example.com/cn=Test%20User")
.add_parameter("LANGUAGE", "en")
.done();
let attendee = Attendee::try_from(&prop).unwrap();
assert_eq!(attendee.cal_address, "mailto:test@example.com");
assert_eq!(attendee.cutype, Some(CUType::Individual));
assert_eq!(attendee.role, Some(Role::ReqParticipant));
assert_eq!(attendee.part_stat, Some(PartStat::Accepted));
assert_eq!(attendee.rsvp, Some(true));
assert_eq!(attendee.cn, Some("Test User".to_string()));
assert_eq!(
attendee.member,
vec![
"mailto:member1@example.com".to_string(),
"mailto:member2@example.com".to_string()
]
);
assert_eq!(
attendee.delegated_to,
vec!["mailto:delegate@example.com".to_string()]
);
assert_eq!(
attendee.delegated_from,
vec!["mailto:delegator@example.com".to_string()]
);
assert_eq!(
attendee.sent_by,
Some("mailto:sender@example.com".to_string())
);
assert_eq!(
attendee.dir,
Some("ldap://example.com/cn=Test%20User".to_string())
);
assert_eq!(attendee.language, Some("en".to_string()));
}
#[test]
fn try_from_invalid_property() {
let prop = Property::new("NOT_ATTENDEE", "mailto:test@example.com").done();
assert!(Attendee::try_from(&prop).is_err());
}
#[test]
fn from_property_unquoted_multi_value() {
let prop = Property::new("ATTENDEE", "mailto:test@example.com")
.add_parameter(
"MEMBER",
"mailto:member1@example.com,mailto:member2@example.com",
)
.done();
let attendee = Attendee::try_from(&prop).unwrap();
assert_eq!(
attendee.member,
vec![
"mailto:member1@example.com".to_string(),
"mailto:member2@example.com".to_string()
]
);
}
#[test]
fn roundtrip() {
let original = Attendee::new("mailto:roundtrip@example.com".to_string())
.cutype(CUType::Resource)
.role(Role::OptParticipant)
.partstat(PartStat::Declined)
.rsvp(true)
.cn("Roundtrip User".to_string())
.member("mailto:rt_member@example.com".to_string())
.delegated_to("mailto:rt_delegate@example.com".to_string())
.sentby("mailto:rt_sender@example.com".to_string())
.dir("ldap://rt.example.com".to_string())
.language("de".to_string());
let prop: Property = original.clone().into();
let reconstructed = Attendee::try_from(&prop).unwrap();
assert_eq!(original, reconstructed);
}
#[test]
fn from_str_case_insensitive() {
assert_eq!(CUType::from_str("individual"), Some(CUType::Individual));
assert_eq!(CUType::from_str("Group"), Some(CUType::Group));
assert_eq!(Role::from_str("chair"), Some(Role::Chair));
assert_eq!(
Role::from_str("req-participant"),
Some(Role::ReqParticipant)
);
assert_eq!(PartStat::from_str("accepted"), Some(PartStat::Accepted));
assert_eq!(PartStat::from_str("In-Process"), Some(PartStat::InProcess));
}
}