Add the iCalendar round-trip
Parsing goes through icalendar's low-level parser, which keeps properties in order and keeps repeated ones. Writing is ours: that crate's writer escapes a whole property value as text, so CATEGORIES:Work,Personal would go out as one category named "Work,Personal" to every other client. Anything the model does not interpret is carried rather than dropped -- X-MOZ-LASTACK, X-EVOLUTION-ALARM-UID, ACKNOWLEDGED, the X-MICROSOFT-CDO set, unrecognised ATTENDEE parameters, and whole VTODO/VJOURNAL components. A calendar has several clients writing to it and this one is not the authority on which properties matter. VTIMEZONE is modelled properly, and TZID is stored exactly as written: Exchange names its zones "Pacific Standard Time", which no IANA lookup resolves, and normalising at parse time would make the document unrepresentable. Mapping to a real zone belongs at the point of use. Tested against a golden corpus captured from the live Baikal (seven producing clients over five years) and a published Outlook feed, scrubbed of private content with the structure left byte-for-byte. Eight hand-written fixtures cover what neither server had: DURATION, floating times, RDATE, DST boundaries, leap day, and the full escape set. The contract is that parse -> write -> parse is stable, plus a check that no property name loses occurrences across the trip, since a parser that dropped ATTENDEE entirely would round-trip perfectly and still be wrong.
This commit is contained in:
@@ -443,7 +443,7 @@ fn series_with_one_override() -> CalendarObject {
|
||||
href: "/calendars/connor/personal/shared-uid.ics".to_owned(),
|
||||
etag: Some("\"abc123\"".to_owned()),
|
||||
calendar_path: "/calendars/connor/personal/".to_owned(),
|
||||
events: vec![master, moved],
|
||||
calendar: VCalendar::with_events(vec![master, moved]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ fn a_resource_separates_its_master_from_its_overrides() {
|
||||
assert!(object.master().is_some());
|
||||
assert_eq!(object.overrides().count(), 1);
|
||||
assert_eq!(
|
||||
object.events.len(),
|
||||
object.events().len(),
|
||||
2,
|
||||
"a master and its override are two VEVENTs in one resource, not a \
|
||||
duplicate to be deduplicated away",
|
||||
@@ -472,7 +472,7 @@ fn every_event_in_a_resource_shares_one_uid() {
|
||||
#[test]
|
||||
fn a_resource_with_mismatched_uids_is_detected() {
|
||||
let mut object = series_with_one_override();
|
||||
object.events[1].uid = "different".to_owned();
|
||||
object.calendar.events[1].uid = "different".to_owned();
|
||||
|
||||
assert!(
|
||||
!object.has_consistent_uid(),
|
||||
@@ -484,7 +484,7 @@ fn a_resource_with_mismatched_uids_is_detected() {
|
||||
#[test]
|
||||
fn a_resource_holding_only_overrides_has_no_master() {
|
||||
let mut object = series_with_one_override();
|
||||
object.events.retain(VEvent::is_override);
|
||||
object.calendar.events.retain(VEvent::is_override);
|
||||
|
||||
assert!(object.master().is_none());
|
||||
assert_eq!(object.overrides().count(), 1);
|
||||
|
||||
Reference in New Issue
Block a user