ical: Fix data model to allow calendar objects with overrides

#125
This commit is contained in:
Lennart
2025-09-18 20:38:37 +02:00
parent 55dadbb06b
commit cb1356acad
6 changed files with 311 additions and 127 deletions

View File

@@ -15,6 +15,10 @@ pub struct EventObject {
}
impl EventObject {
pub fn get_uid(&self) -> &str {
self.event.get_uid()
}
pub fn get_dtstart(&self) -> Result<Option<CalDateTime>, Error> {
if let Some(dtstart) = self.event.get_dtstart() {
Ok(Some(CalDateTime::parse_prop(dtstart, &self.timezones)?))
@@ -92,6 +96,7 @@ impl EventObject {
&self,
start: Option<DateTime<Utc>>,
end: Option<DateTime<Utc>>,
overrides: &[EventObject],
) -> Result<Vec<IcalEvent>, Error> {
if let Some(mut rrule_set) = self.recurrence_ruleset()? {
if let Some(start) = start {
@@ -107,13 +112,30 @@ impl EventObject {
.get_dtend()?
.map(|dtend| dtend.as_datetime().into_owned() - dtstart.as_datetime().into_owned());
for date in dates {
'recurrence: for date in dates {
let date = CalDateTime::from(date);
let dateformat = if dtstart.is_date() {
date.format_date()
} else {
date.format()
};
for _override in overrides {
if let Some(override_id) = &_override
.event
.get_recurrence_id()
.as_ref()
.expect("overrides have a recurrence id")
.value
&& override_id == &dateformat
{
// We have an override for this occurence
//
events.push(_override.event.clone());
continue 'recurrence;
}
}
let mut ev = self.event.clone().mutable();
ev.remove_property("RRULE");
ev.remove_property("RDATE");
@@ -229,10 +251,18 @@ END:VEVENT\r\n",
#[test]
fn test_expand_recurrence() {
let event = CalendarObject::from_ics(ICS.to_string()).unwrap();
let event = event.event().unwrap();
let (event, overrides) = if let crate::CalendarObjectComponent::Event(
main_event,
overrides,
) = event.get_data()
{
(main_event, overrides)
} else {
panic!()
};
let events: Vec<String> = event
.expand_recurrence(None, None)
.expand_recurrence(None, None, overrides)
.unwrap()
.into_iter()
.map(|event| Emitter::generate(&event))

View File

@@ -3,3 +3,9 @@ use ical::parser::ical::component::IcalJournal;
#[derive(Debug, Clone, From)]
pub struct JournalObject(pub IcalJournal);
impl JournalObject {
pub fn get_uid(&self) -> &str {
self.0.get_uid()
}
}

View File

@@ -56,18 +56,75 @@ impl rustical_xml::ValueDeserialize for CalendarObjectType {
#[derive(Debug, Clone)]
pub enum CalendarObjectComponent {
Event(EventObject),
Todo(TodoObject),
Journal(JournalObject),
Event(EventObject, Vec<EventObject>),
Todo(TodoObject, Vec<TodoObject>),
Journal(JournalObject, Vec<JournalObject>),
}
impl Default for CalendarObjectComponent {
fn default() -> Self {
Self::Event(EventObject::default())
impl CalendarObjectComponent {
fn from_events(mut events: Vec<EventObject>) -> Result<Self, Error> {
let main_event = events
.extract_if(.., |event| event.event.get_recurrence_id().is_none())
.next()
.expect("there must be one main event");
let overrides = events;
for event in &overrides {
if event.get_uid() != main_event.get_uid() {
return Err(Error::InvalidData(
"Calendar object contains multiple UIDs".to_owned(),
));
}
if event.event.get_recurrence_id().is_none() {
return Err(Error::InvalidData(
"Calendar object can only contain one main component".to_owned(),
));
}
}
Ok(Self::Event(main_event, overrides))
}
fn from_todos(mut todos: Vec<TodoObject>) -> Result<Self, Error> {
let main_todo = todos
.extract_if(.., |todo| todo.0.get_recurrence_id().is_none())
.next()
.expect("there must be one main event");
let overrides = todos;
for todo in &overrides {
if todo.get_uid() != main_todo.get_uid() {
return Err(Error::InvalidData(
"Calendar object contains multiple UIDs".to_owned(),
));
}
if todo.0.get_recurrence_id().is_none() {
return Err(Error::InvalidData(
"Calendar object can only contain one main component".to_owned(),
));
}
}
Ok(Self::Todo(main_todo, overrides))
}
fn from_journals(mut journals: Vec<JournalObject>) -> Result<Self, Error> {
let main_journal = journals
.extract_if(.., |journal| journal.0.get_recurrence_id().is_none())
.next()
.expect("there must be one main event");
let overrides = journals;
for journal in &overrides {
if journal.get_uid() != main_journal.get_uid() {
return Err(Error::InvalidData(
"Calendar object contains multiple UIDs".to_owned(),
));
}
if journal.0.get_recurrence_id().is_none() {
return Err(Error::InvalidData(
"Calendar object can only contain one main component".to_owned(),
));
}
}
Ok(Self::Journal(main_journal, overrides))
}
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone)]
pub struct CalendarObject {
data: CalendarObjectComponent,
properties: Vec<Property>,
@@ -84,16 +141,16 @@ impl CalendarObject {
"multiple calendars, only one allowed".to_owned(),
));
}
if cal.events.len()
+ cal.alarms.len()
+ cal.todos.len()
+ cal.journals.len()
+ cal.free_busys.len()
if !cal.events.is_empty() as u8
+ !cal.todos.is_empty() as u8
+ !cal.journals.is_empty() as u8
+ !cal.free_busys.is_empty() as u8
!= 1
{
// https://datatracker.ietf.org/doc/html/rfc4791#section-4.1
return Err(Error::InvalidData(
"iCalendar object is only allowed to have exactly one component".to_owned(),
"iCalendar object must have exactly one component type".to_owned(),
));
}
@@ -111,12 +168,27 @@ impl CalendarObject {
.map(|timezone| (timezone.get_tzid().to_owned(), timezone))
.collect();
let data = if let Some(event) = cal.events.into_iter().next() {
CalendarObjectComponent::Event(EventObject { event, timezones })
} else if let Some(todo) = cal.todos.into_iter().next() {
CalendarObjectComponent::Todo(todo.into())
} else if let Some(journal) = cal.journals.into_iter().next() {
CalendarObjectComponent::Journal(journal.into())
let data = if !cal.events.is_empty() {
CalendarObjectComponent::from_events(
cal.events
.into_iter()
.map(|event| EventObject {
event,
timezones: timezones.clone(),
})
.collect(),
)?
} else if !cal.todos.is_empty() {
CalendarObjectComponent::from_todos(
cal.todos.into_iter().map(|todo| todo.into()).collect(),
)?
} else if !cal.journals.is_empty() {
CalendarObjectComponent::from_journals(
cal.journals
.into_iter()
.map(|journal| journal.into())
.collect(),
)?
} else {
return Err(Error::InvalidData(
"iCalendar component type not supported :(".to_owned(),
@@ -141,9 +213,11 @@ impl CalendarObject {
pub fn get_id(&self) -> &str {
match &self.data {
CalendarObjectComponent::Todo(todo) => todo.0.get_uid(),
CalendarObjectComponent::Event(event) => event.event.get_uid(),
CalendarObjectComponent::Journal(journal) => journal.0.get_uid(),
// We've made sure before that the first component exists and all components share the
// same UID
CalendarObjectComponent::Todo(todo, _) => todo.0.get_uid(),
CalendarObjectComponent::Event(event, _) => event.event.get_uid(),
CalendarObjectComponent::Journal(journal, _) => journal.0.get_uid(),
}
}
@@ -164,33 +238,40 @@ impl CalendarObject {
pub fn get_object_type(&self) -> CalendarObjectType {
match self.data {
CalendarObjectComponent::Todo(_) => CalendarObjectType::Todo,
CalendarObjectComponent::Event(_) => CalendarObjectType::Event,
CalendarObjectComponent::Journal(_) => CalendarObjectType::Journal,
CalendarObjectComponent::Todo(_, _) => CalendarObjectType::Todo,
CalendarObjectComponent::Event(_, _) => CalendarObjectType::Event,
CalendarObjectComponent::Journal(_, _) => CalendarObjectType::Journal,
}
}
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
match &self.data {
CalendarObjectComponent::Event(event) => event.get_dtstart(),
CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides
.iter()
.chain([main_event].into_iter())
.map(|event| event.get_dtstart())
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.min()),
_ => Ok(None),
}
}
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
match &self.data {
CalendarObjectComponent::Event(event) => event.get_last_occurence(),
CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides
.iter()
.chain([main_event].into_iter())
.map(|event| event.get_last_occurence())
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.max()),
_ => Ok(None),
}
}
pub fn event(&self) -> Option<&EventObject> {
match &self.data {
CalendarObjectComponent::Event(event) => Some(event),
_ => None,
}
}
pub fn expand_recurrence(
&self,
start: Option<DateTime<Utc>>,
@@ -198,10 +279,10 @@ impl CalendarObject {
) -> Result<String, Error> {
// Only events can be expanded
match &self.data {
CalendarObjectComponent::Event(event) => {
CalendarObjectComponent::Event(main_event, overrides) => {
let cal = IcalCalendar {
properties: self.properties.clone(),
events: event.expand_recurrence(start, end)?,
events: main_event.expand_recurrence(start, end, overrides)?,
..Default::default()
};
Ok(cal.generate())

View File

@@ -3,3 +3,9 @@ use ical::parser::ical::component::IcalTodo;
#[derive(Debug, Clone, From)]
pub struct TodoObject(pub IcalTodo);
impl TodoObject {
pub fn get_uid(&self) -> &str {
self.0.get_uid()
}
}

View File

@@ -0,0 +1,30 @@
use rustical_ical::CalendarObject;
const MULTI_VEVENT: &str = r#"
BEGIN:VCALENDAR
PRODID:-//Example Corp.//CalDAV Client//EN
VERSION:2.0
BEGIN:VEVENT
UID:2@example.com
SUMMARY:Weekly Meeting
DTSTAMP:20041210T183838Z
DTSTART:20041206T120000Z
DTEND:20041206T130000Z
RRULE:FREQ=WEEKLY
END:VEVENT
BEGIN:VEVENT
UID:2@example.com
SUMMARY:Weekly Meeting
RECURRENCE-ID:20041213T120000Z
DTSTAMP:20041210T183838Z
DTSTART:20041213T130000Z
DTEND:20041213T140000Z
END:VEVENT
END:VCALENDAR
"#;
#[test]
fn parse_calendar_object() {
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string()).unwrap();
object.expand_recurrence(None, None).unwrap();
}