feat: RECURRENCE-ID parsing, serialization, and round-trip support
Add recurrence_id field to Event domain model. Parse and serialize RECURRENCE-ID from/to iCalendar files. Override events (with RECURRENCE-ID but no RRULE) are safe to rewrite. Save validation rejects events with both RECURRENCE-ID and RRULE.
This commit is contained in:
@@ -50,6 +50,7 @@ struct PendingEvent {
|
||||
std::string color;
|
||||
std::optional<ParsedTime> start;
|
||||
std::optional<ParsedTime> end;
|
||||
std::optional<ParsedTime> recurrence_id;
|
||||
std::vector<PendingProperty> recurrence_rules;
|
||||
std::vector<PendingProperty> recurrence_dates;
|
||||
std::vector<PendingProperty> exception_dates;
|
||||
@@ -767,6 +768,12 @@ void serialize_calendar(std::ostream& output, std::span<const Event> events,
|
||||
for (const Event& event : events) {
|
||||
write_folded(output, "BEGIN:VEVENT");
|
||||
write_folded(output, "UID:" + escape_text(event.uid));
|
||||
if (event.recurrence_id) {
|
||||
const std::string params = event.all_day ? ";VALUE=DATE" : time_property_parameters(event);
|
||||
write_folded(output, "RECURRENCE-ID" + params + ":"
|
||||
+ (event.all_day ? format_date(*event.recurrence_id)
|
||||
: format_event_time(event, *event.recurrence_id)));
|
||||
}
|
||||
if (event.all_day) {
|
||||
write_folded(output, "DTSTART;VALUE=DATE:" + format_date(event.start));
|
||||
write_folded(output, "DTEND;VALUE=DATE:" + format_date(event.end));
|
||||
@@ -1113,6 +1120,11 @@ void validate_event_for_save(
|
||||
if (event.uid.empty()) {
|
||||
throw std::invalid_argument("cannot save an event with an empty UID");
|
||||
}
|
||||
if (event.recurrence_id && event.recurrence) {
|
||||
throw std::invalid_argument(
|
||||
"cannot save override event '" + event.uid
|
||||
+ "' with both RECURRENCE-ID and RRULE");
|
||||
}
|
||||
if (event.end < event.start || (event.all_day && event.end == event.start)) {
|
||||
throw std::invalid_argument(
|
||||
"cannot save event '" + event.uid + "' with an invalid interval");
|
||||
@@ -1278,6 +1290,9 @@ LoadResult IcsStore::load(const std::filesystem::path& path) {
|
||||
event.description = std::move(pending->description);
|
||||
event.calendar_id = std::move(pending->calendar_id);
|
||||
event.color = std::move(pending->color);
|
||||
if (pending->recurrence_id) {
|
||||
event.recurrence_id = pending->recurrence_id->value;
|
||||
}
|
||||
|
||||
if (pending->recurrence_rules.size() > 1) {
|
||||
add_warning(result, pending->recurrence_rules[1].line,
|
||||
@@ -1414,7 +1429,7 @@ LoadResult IcsStore::load(const std::filesystem::path& path) {
|
||||
continue;
|
||||
}
|
||||
const bool time_property = name == "DTSTART" || name == "DTEND"
|
||||
|| name == "RDATE" || name == "EXDATE";
|
||||
|| name == "RDATE" || name == "EXDATE" || name == "RECURRENCE-ID";
|
||||
const bool recurrence_property = name == "RRULE";
|
||||
const bool text_property = name == "UID" || name == "SUMMARY" || name == "LOCATION"
|
||||
|| name == "DESCRIPTION" || name == "X-NOCAL-CALENDAR-ID"
|
||||
@@ -1472,6 +1487,26 @@ LoadResult IcsStore::load(const std::filesystem::path& path) {
|
||||
}
|
||||
destination = *parsed;
|
||||
}
|
||||
} else if (name == "RECURRENCE-ID") {
|
||||
if (pending->recurrence_id) {
|
||||
add_warning(result, index + 1, "duplicate RECURRENCE-ID property");
|
||||
mark_unsafe_to_rewrite(result);
|
||||
}
|
||||
std::string warning;
|
||||
auto parsed = parse_time(parameters, value, warning);
|
||||
if (!parsed) {
|
||||
add_warning(result, index + 1, std::move(warning));
|
||||
mark_unsafe_to_rewrite(result);
|
||||
} else {
|
||||
if (!warning.empty()) {
|
||||
const bool is_tzid = is_unknown_tzid_warning(warning);
|
||||
add_warning(result, index + 1, std::move(warning));
|
||||
if (!is_tzid) {
|
||||
mark_unsafe_to_rewrite(result);
|
||||
}
|
||||
}
|
||||
pending->recurrence_id = *parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user