#[cfg(feature = "recurrence")]
use crate::recurrence::RecurrenceError;
use chrono::{DateTime, NaiveDate, Utc};
use uuid::Uuid;
use std::{collections::BTreeMap, fmt, mem};
use crate::{Attendee, properties::*};
use date_time::{format_utc_date_time, naive_date_to_property, parse_utc_date_time};
pub mod alarm;
pub(crate) mod date_time;
mod event;
mod other;
mod todo;
mod venue;
use alarm::*;
use date_time::{CalendarDateTime, DatePerhapsTime};
pub use event::*;
pub use other::*;
pub use todo::*;
pub use venue::*;
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub(crate) struct InnerComponent {
pub properties: BTreeMap<String, Property>,
pub multi_properties: BTreeMap<String, Vec<Property>>,
pub components: Vec<Other>,
}
impl From<Other> for InnerComponent {
fn from(val: Other) -> Self {
val.inner
}
}
impl InnerComponent {
pub fn done(&mut self) -> Self {
InnerComponent {
properties: mem::take(&mut self.properties),
multi_properties: mem::take(&mut self.multi_properties),
components: mem::take(&mut self.components),
}
}
pub(crate) fn insert_multi(&mut self, property: impl Into<Property>) -> &mut Self {
let property = property.into();
let key = property.key().to_owned();
self.multi_properties
.entry(key)
.and_modify(|v| v.push(property.to_owned()))
.or_insert(vec![property.to_owned()]);
self
}
#[cfg(test)]
pub fn property_value(&self, key: &str) -> Option<&str> {
Some(self.properties.get(key)?.value())
}
}
pub trait Component {
fn component_kind(&self) -> String;
fn properties(&self) -> &BTreeMap<String, Property>;
fn components(&self) -> &[Other];
fn multi_properties(&self) -> &BTreeMap<String, Vec<Property>>;
fn property_value(&self, key: &str) -> Option<&str> {
Some(self.properties().get(key)?.value())
}
fn fmt_write<W: fmt::Write>(&self, out: &mut W) -> Result<(), fmt::Error> {
write_crlf!(out, "BEGIN:{}", self.component_kind())?;
if !self.properties().contains_key("DTSTAMP") {
let now = Utc::now();
write_crlf!(out, "DTSTAMP:{}", format_utc_date_time(now))?;
}
for property in self.properties().values() {
property.fmt_write(out)?;
}
if !self.properties().contains_key("UID") {
write_crlf!(out, "UID:{}", Uuid::new_v4())?;
}
for property in self.multi_properties().values().flatten() {
property.fmt_write(out)?;
}
for component in self.components() {
component.fmt_write(out)?;
}
write_crlf!(out, "END:{}", self.component_kind())?;
Ok(())
}
fn to_string(&self) -> String {
self.try_into_string().unwrap()
}
fn try_into_string(&self) -> Result<String, fmt::Error> {
let mut out_string = String::new();
self.fmt_write(&mut out_string)?;
Ok(out_string)
}
fn append_property(&mut self, property: impl Into<Property>) -> &mut Self;
fn append_component(&mut self, child: impl Into<Other>) -> &mut Self;
fn append_multi_property(&mut self, property: impl Into<Property>) -> &mut Self;
fn add_property(&mut self, key: impl Into<String>, val: impl Into<String>) -> &mut Self {
self.append_property(Property::new(key, val))
}
fn remove_property(&mut self, key: &str) -> &mut Self;
fn remove_multi_property(&mut self, key: &str) -> &mut Self;
#[deprecated]
fn add_property_pre_alloc(&mut self, key: String, val: String) -> &mut Self {
self.append_property(Property::new(key, val))
}
fn add_multi_property(&mut self, key: &str, val: &str) -> &mut Self {
self.append_multi_property(Property::new(key, val))
}
fn timestamp(&mut self, dt: DateTime<Utc>) -> &mut Self {
self.add_property("DTSTAMP", format_utc_date_time(dt))
}
fn remove_timestamp(&mut self) -> &mut Self {
self.remove_property("DTSTAMP")
}
fn get_timestamp(&self) -> Option<DateTime<Utc>> {
parse_utc_date_time(self.property_value("DTSTAMP")?)
}
fn get_start(&self) -> Option<DatePerhapsTime> {
DatePerhapsTime::from_property(self.properties().get("DTSTART")?)
}
fn get_recurrence_id(&self) -> Option<DatePerhapsTime> {
DatePerhapsTime::from_property(self.properties().get("RECURRENCE-ID")?)
}
fn get_end(&self) -> Option<DatePerhapsTime> {
DatePerhapsTime::from_property(self.properties().get("DTEND")?)
}
fn priority(&mut self, priority: u32) -> &mut Self {
let priority = std::cmp::min(priority, 10);
self.add_property("PRIORITY", priority.to_string())
}
fn remove_priority(&mut self) -> &mut Self {
self.remove_property("PRIORITY")
}
fn get_priority(&self) -> Option<u32> {
let priority = self.property_value("PRIORITY")?.parse().ok()?;
if priority <= 10 { Some(priority) } else { None }
}
fn print(&self) -> Result<(), fmt::Error> {
let mut out = String::new();
self.fmt_write(&mut out)?;
print_crlf!("{}", out);
Ok(())
}
fn summary(&mut self, desc: &str) -> &mut Self {
self.add_property("SUMMARY", desc)
}
fn remove_summary(&mut self) -> &mut Self {
self.remove_property("SUMMARY")
}
fn get_summary(&self) -> Option<&str> {
self.property_value("SUMMARY")
}
fn description(&mut self, desc: &str) -> &mut Self {
self.add_property("DESCRIPTION", desc)
}
fn remove_description(&mut self) -> &mut Self {
self.remove_property("DESCRIPTION")
}
fn get_description(&self) -> Option<&str> {
self.property_value("DESCRIPTION")
}
fn attendee(&mut self, attendee: Attendee) -> &mut Self {
self.append_multi_property(attendee)
}
fn get_attendees(&self) -> Vec<Attendee> {
self.multi_properties()
.get("ATTENDEE")
.map(|v| {
v.iter()
.filter_map(|p| Attendee::try_from(p).ok())
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
fn uid(&mut self, uid: &str) -> &mut Self {
self.add_property("UID", uid)
}
fn get_uid(&self) -> Option<&str> {
self.property_value("UID")
}
fn sequence(&mut self, sequence: u32) -> &mut Self {
self.add_property("SEQUENCE", sequence.to_string())
}
fn remove_sequence(&mut self) -> &mut Self {
self.remove_property("SEQUENCE")
}
fn get_sequence(&self) -> Option<u32> {
self.property_value("SEQUENCE").and_then(|s| s.parse().ok())
}
fn class(&mut self, class: Class) -> &mut Self {
self.append_property(class)
}
fn remove_class(&mut self) -> &mut Self {
self.remove_property("CLASS")
}
fn get_class(&self) -> Option<Class> {
Class::from_str(self.property_value("CLASS")?)
}
fn url(&mut self, url: &str) -> &mut Self {
self.add_property("URL", url)
}
fn remove_url(&mut self) -> &mut Self {
self.remove_property("URL")
}
fn get_url(&self) -> Option<&str> {
self.property_value("URL")
}
fn last_modified(&mut self, dt: DateTime<Utc>) -> &mut Self {
self.add_property("LAST-MODIFIED", format_utc_date_time(dt))
}
fn remove_last_modified(&mut self) -> &mut Self {
self.remove_property("LAST-MODIFIED")
}
fn get_last_modified(&self) -> Option<DateTime<Utc>> {
parse_utc_date_time(self.property_value("LAST-MODIFIED")?)
}
fn created(&mut self, dt: DateTime<Utc>) -> &mut Self {
self.add_property("CREATED", format_utc_date_time(dt))
}
fn remove_created(&mut self) -> &mut Self {
self.remove_property("CREATED")
}
fn get_created(&self) -> Option<DateTime<Utc>> {
parse_utc_date_time(self.property_value("CREATED")?)
}
}
pub trait EventLike: Component {
fn starts<T: Into<DatePerhapsTime>>(&mut self, dt: T) -> &mut Self {
let calendar_dt = dt.into();
self.append_property(calendar_dt.to_property("DTSTART"))
}
fn remove_starts(&mut self) -> &mut Self {
self.remove_property("DTSTART")
}
fn ends<T: Into<DatePerhapsTime>>(&mut self, dt: T) -> &mut Self {
let calendar_dt = dt.into();
self.append_property(calendar_dt.to_property("DTEND"))
}
fn remove_ends(&mut self) -> &mut Self {
self.remove_property("DTEND")
}
fn recurrence_id<T: Into<DatePerhapsTime>>(&mut self, dt: T) -> &mut Self {
let calendar_dt = dt.into();
self.append_property(calendar_dt.to_property("RECURRENCE-ID"))
}
fn remove_recurrence_id(&mut self) -> &mut Self {
self.remove_property("RECURRENCE-ID")
}
fn all_day(&mut self, date: NaiveDate) -> &mut Self {
self.append_property(naive_date_to_property(date, "DTSTART"))
.append_property(naive_date_to_property(date, "DTEND"))
}
fn venue(&mut self, location: &str, venue_uid: &str) -> &mut Self {
self.append_property(
Property::new("LOCATION", location)
.append_parameter(Parameter::new("VVENUE", venue_uid))
.done(),
);
self
}
fn location(&mut self, location: &str) -> &mut Self {
self.add_property("LOCATION", location)
}
fn remove_location(&mut self) -> &mut Self {
self.remove_property("LOCATION")
}
fn get_location(&self) -> Option<&str> {
self.property_value("LOCATION")
}
#[cfg(feature = "recurrence")]
fn recurrence(&mut self, rrule: crate::UnvalidatedRRule) -> Result<&mut Self, RecurrenceError> {
let dt_start_prop = self
.properties()
.get("DTSTART")
.ok_or(RecurrenceError::MissingDtStart)?;
let dt_start = crate::recurrence::dt_start_to_rrule_datetime(dt_start_prop)?;
let rruleset = rrule.build(dt_start)?;
let rrule_str = rruleset
.get_rrule()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
self.add_property("RRULE", rrule_str);
Ok(self)
}
#[cfg(feature = "recurrence")]
fn get_recurrence(&self) -> Result<rrule::RRuleSet, RecurrenceError> {
use std::fmt::Write;
let mut b = String::new();
if let Some(dt_start_prop) = self.properties().get("DTSTART") {
if let Some(tzid) = dt_start_prop.params().get("TZID") {
writeln!(b, "DTSTART;TZID={}:{}", tzid.value(), dt_start_prop.value()).unwrap();
} else {
writeln!(b, "DTSTART:{}", dt_start_prop.value()).unwrap();
}
let has_rrule = self.property_value("RRULE").is_some();
if !has_rrule {
if let Some(tzid) = dt_start_prop.params().get("TZID") {
writeln!(b, "RDATE;TZID={}:{}", tzid.value(), dt_start_prop.value()).unwrap();
} else {
writeln!(b, "RDATE:{}", dt_start_prop.value()).unwrap();
}
}
};
if let Some(rrule_str) = self.property_value("RRULE") {
writeln!(b, "RRULE:{rrule_str}").unwrap();
}
if let Some(rdates) = self.multi_properties().get("RDATE") {
for rdate in rdates.iter().filter_map(|p| p.to_line().ok()) {
writeln!(b, "{rdate}").unwrap();
}
}
if let Some(exdates) = self.multi_properties().get("EXDATE") {
for exdate in exdates.iter().filter_map(|p| p.to_line().ok()) {
writeln!(b, "{exdate}").unwrap();
}
}
b.parse::<rrule::RRuleSet>().map_err(RecurrenceError::Rule)
}
fn rdate<T: Into<DatePerhapsTime>>(&mut self, rdate: T) -> &mut Self {
self.append_multi_property(rdate.into().to_property("RDATE"))
}
fn exdate<T: Into<DatePerhapsTime>>(&mut self, exdate: T) -> &mut Self {
self.append_multi_property(exdate.into().to_property("EXDATE"))
}
fn alarm<A: Into<Alarm>>(&mut self, alarm: A) -> &mut Self {
let alarm: Alarm = alarm.into();
self.append_component(alarm)
}
}
macro_rules! event_impl {
($t:ty) => {
impl EventLike for $t {}
};
}
macro_rules! component_impl {
($t:ty, $kind:expr) => {
impl Component for $t {
fn component_kind(&self) -> String {
$kind
}
fn properties(&self) -> &BTreeMap<String, Property> {
&self.inner.properties
}
fn components(&self) -> &[Other] {
&self.inner.components
}
fn multi_properties(&self) -> &BTreeMap<String, Vec<Property>> {
&self.inner.multi_properties
}
fn append_property(&mut self, property: impl Into<Property>) -> &mut Self {
let property = property.into();
self.inner
.properties
.insert(property.key().to_owned(), property);
self
}
fn append_component(&mut self, child: impl Into<Other>) -> &mut Self {
self.inner.components.push(child.into());
self
}
fn append_multi_property(&mut self, property: impl Into<Property>) -> &mut Self {
self.inner.insert_multi(property);
self
}
fn remove_property(&mut self, key: &str) -> &mut Self {
self.inner.properties.remove(key);
self
}
fn remove_multi_property(&mut self, key: &str) -> &mut Self {
self.inner.multi_properties.remove(key);
self
}
}
impl From<InnerComponent> for $t {
fn from(inner: InnerComponent) -> $t {
Self { inner }
}
}
impl From<$t> for Other {
fn from(val: $t) -> Self {
(val.component_kind(), val.inner).into()
}
}
impl TryInto<String> for $t {
type Error = std::fmt::Error;
fn try_into(self) -> Result<String, Self::Error> {
self.try_into_string()
}
}
};
}
component_impl! { Event, String::from("VEVENT") }
event_impl! { Event }
component_impl! { Todo , String::from("VTODO")}
event_impl! { Todo}
component_impl! { Venue , String::from("VVENUE")}
component_impl! { Alarm, String::from("VALARM") }
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use super::*;
#[test]
#[cfg(feature = "parser")]
fn get_url() {
let url = "http://hoodie.de/";
let event = Event::new().url(url).done();
let serialized = event.to_string();
let reparsed =
Other::from(crate::parser::Component::<'_>::try_from(serialized.as_str()).unwrap());
assert_eq!(event.get_url(), Some(url));
assert_eq!(reparsed.get_url(), Some(url));
}
#[test]
fn get_properties_unset() {
let event = Event::new();
assert_eq!(event.get_priority(), None);
assert_eq!(event.get_summary(), None);
assert_eq!(event.get_description(), None);
assert_eq!(event.get_location(), None);
assert_eq!(event.get_uid(), None);
assert_eq!(event.get_class(), None);
assert_eq!(event.get_timestamp(), None);
assert_eq!(event.get_last_modified(), None);
assert_eq!(event.get_created(), None);
assert_eq!(event.get_url(), None);
}
#[test]
fn get_properties_set() {
let event = Event::new()
.priority(5)
.summary("summary")
.description("description")
.location("location")
.uid("uid")
.class(Class::Private)
.url("http://some.test/url")
.done();
assert_eq!(event.get_priority(), Some(5));
assert_eq!(event.get_summary(), Some("summary"));
assert_eq!(event.get_description(), Some("description"));
assert_eq!(event.get_location(), Some("location"));
assert_eq!(event.get_uid(), Some("uid"));
assert_eq!(event.get_class(), Some(Class::Private));
assert_eq!(event.get_url(), Some("http://some.test/url"));
}
#[test]
fn get_properties_remove() {
let mut event = Event::new()
.priority(5)
.summary("summary")
.description("description")
.location("location")
.uid("uid")
.class(Class::Private)
.url("http://some.test/url")
.done();
assert_eq!(event.get_priority(), Some(5));
assert_eq!(event.get_summary(), Some("summary"));
assert_eq!(event.get_description(), Some("description"));
assert_eq!(event.get_location(), Some("location"));
assert_eq!(event.get_uid(), Some("uid"));
assert_eq!(event.get_class(), Some(Class::Private));
assert_eq!(event.get_url(), Some("http://some.test/url"));
event
.remove_priority()
.remove_summary()
.remove_description()
.remove_location()
.remove_class()
.remove_url();
assert_eq!(event.get_priority(), None);
assert_eq!(event.get_summary(), None);
assert_eq!(event.get_description(), None);
assert_eq!(event.get_location(), None);
assert_eq!(event.get_class(), None);
assert_eq!(event.get_url(), None);
}
#[test]
fn get_date_times_naive() {
let naive_date_time = NaiveDate::from_ymd_opt(2001, 3, 13)
.unwrap()
.and_hms_opt(14, 15, 16)
.unwrap();
let event = Event::new()
.starts(naive_date_time)
.ends(naive_date_time)
.done();
assert_eq!(event.get_start(), Some(naive_date_time.into()));
assert_eq!(event.get_end(), Some(naive_date_time.into()));
}
#[test]
fn get_date_times_utc() {
let utc_date_time = Utc.with_ymd_and_hms(2001, 3, 13, 14, 15, 16).unwrap();
let event = Event::new()
.timestamp(utc_date_time)
.last_modified(utc_date_time)
.created(utc_date_time)
.starts(utc_date_time)
.ends(utc_date_time)
.done();
assert_eq!(event.get_timestamp(), Some(utc_date_time));
assert_eq!(event.get_last_modified(), Some(utc_date_time));
assert_eq!(event.get_created(), Some(utc_date_time));
assert_eq!(event.get_start(), Some(utc_date_time.into()));
assert_eq!(event.get_end(), Some(utc_date_time.into()));
}
#[test]
fn get_date_times_tzid() {
let date_time = NaiveDate::from_ymd_opt(2001, 3, 13)
.unwrap()
.and_hms_opt(14, 15, 16)
.unwrap();
let date_time_tzid = CalendarDateTime::WithTimezone {
date_time,
tzid: "Pacific/Auckland".to_string(),
};
let event = Event::new()
.starts(date_time_tzid.clone())
.ends(date_time_tzid.clone())
.done();
assert_eq!(event.get_start(), Some(date_time_tzid.clone().into()));
assert_eq!(event.get_end(), Some(date_time_tzid.into()));
}
#[test]
fn get_dates_naive() {
let naive_date = NaiveDate::from_ymd_opt(2001, 3, 13).unwrap();
let event = Event::new().starts(naive_date).ends(naive_date).done();
assert_eq!(event.get_start(), Some(naive_date.into()));
assert_eq!(event.get_end(), Some(naive_date.into()));
}
#[test]
fn exdate_accepts_naive_date() {
let naive_date = NaiveDate::from_ymd_opt(2001, 3, 13).unwrap();
let event = Event::new().exdate(naive_date).done();
let exdates = event.multi_properties().get("EXDATE").unwrap();
assert_eq!(exdates.len(), 1);
let line = exdates.first().unwrap().to_line().unwrap();
assert!(
line.contains("VALUE=DATE"),
"EXDATE should carry VALUE=DATE parameter, got: {line}"
);
assert_eq!(exdates.first().unwrap().value(), "20010313");
}
#[test]
fn rdate_accepts_naive_date() {
let naive_date = NaiveDate::from_ymd_opt(2001, 3, 13).unwrap();
let event = Event::new().rdate(naive_date).done();
let rdates = event.multi_properties().get("RDATE").unwrap();
assert_eq!(rdates.len(), 1);
let line = rdates.first().unwrap().to_line().unwrap();
assert!(
line.contains("VALUE=DATE"),
"RDATE should carry VALUE=DATE parameter, got: {line}"
);
assert_eq!(rdates.first().unwrap().value(), "20010313");
}
#[test]
#[cfg(feature = "recurrence")]
fn get_recurrence() {
use crate::{Frequency, NWeekday, RRule, Weekday};
let naive_date = NaiveDate::from_ymd_opt(2001, 3, 13).unwrap();
let rrule = RRule::default()
.count(4)
.freq(Frequency::Weekly)
.by_weekday(vec![
NWeekday::Every(Weekday::Tue),
NWeekday::Every(Weekday::Wed),
]);
let event = Event::new()
.starts(naive_date)
.ends(naive_date)
.recurrence(rrule)
.unwrap()
.done();
let output = event
.get_recurrence()
.expect("event should have a recurrence rule");
let output_rrules = output.get_rrule();
assert_eq!(output_rrules.len(), 1);
assert_eq!(output_rrules.first().unwrap().get_freq(), Frequency::Weekly);
assert_eq!(output_rrules.first().unwrap().get_interval(), 1);
assert_eq!(
output_rrules.first().unwrap().get_by_weekday(),
[NWeekday::Every(Weekday::Tue), NWeekday::Every(Weekday::Wed)]
);
}
#[test]
#[cfg(feature = "recurrence")]
fn no_empty_rdate_or_exdate_added() {
use crate::{Frequency, RRule};
let naive_date = NaiveDate::from_ymd_opt(2026, 3, 30).unwrap();
let event = Event::new()
.starts(naive_date)
.recurrence(RRule::default().count(3).freq(Frequency::Daily))
.unwrap()
.done();
let serialized = event.to_string();
assert!(!serialized.contains("RDATE:"));
assert!(!serialized.contains("EXDATE:"));
}
}