Adds ToIcal::to_ical (extension trait on nuncio_core::model::CalendarEvent, implemented in crates/nuncio-cal/src/ical.rs), the missing serializer needed for CalDAV write-back.
Serializes a single VEVENT (UID, DTSTAMP, DTSTART, DTEND, SUMMARY, LOCATION, RRULE) wrapped in a VCALENDAR shell (VERSION:2.0, PRODID), matching the shape IcalParserAdapter::parse_ical consumes.
DTSTART/DTEND are always emitted as UTC Z-suffixed DATE-TIME values: CalendarEvent only stores a resolved UTC unix timestamp (the parser already collapses TZID/floating/Z forms down to one UTC instant), so re-parsing the emitted UTC form always reproduces the same instant regardless of the original wire form.
TEXT values (SUMMARY, LOCATION) are escaped per RFC 5545 3.3.11: backslash, semicolon, comma, and embedded newlines (\n); RRULE is left unescaped since it is a structured value, not TEXT.
An extension trait (rather than an inherent impl) is used because CalendarEvent is owned by nuncio-core, and Rust forbids inherent impls on foreign types.
Test plan
to_ical_roundtrips_through_parse_ical_for_all_event_shapes — round-trips parse_ical(to_ical(event)) back to an equal event for: a plain timed event, an all-day (whole-UTC-day) event, an event whose UTC instant originated from a TZID source, and a recurring event carrying an RRULE.
to_ical_escapes_rfc5545_special_characters_in_text_fields — asserts the literal escaped SUMMARY/LOCATION output and that the escaped payload still round-trips.
to_ical_wraps_a_single_vevent_in_a_vcalendar_shell — asserts the VCALENDAR/VERSION:2.0/single-VEVENT shape.
## Summary
- Adds `ToIcal::to_ical` (extension trait on `nuncio_core::model::CalendarEvent`, implemented in `crates/nuncio-cal/src/ical.rs`), the missing serializer needed for CalDAV write-back.
- Serializes a single `VEVENT` (`UID`, `DTSTAMP`, `DTSTART`, `DTEND`, `SUMMARY`, `LOCATION`, `RRULE`) wrapped in a `VCALENDAR` shell (`VERSION:2.0`, `PRODID`), matching the shape `IcalParserAdapter::parse_ical` consumes.
- `DTSTART`/`DTEND` are always emitted as UTC `Z`-suffixed `DATE-TIME` values: `CalendarEvent` only stores a resolved UTC unix timestamp (the parser already collapses `TZID`/floating/`Z` forms down to one UTC instant), so re-parsing the emitted UTC form always reproduces the same instant regardless of the original wire form.
- `TEXT` values (`SUMMARY`, `LOCATION`) are escaped per RFC 5545 3.3.11: backslash, semicolon, comma, and embedded newlines (`\n`); `RRULE` is left unescaped since it is a structured value, not `TEXT`.
- An extension trait (rather than an inherent impl) is used because `CalendarEvent` is owned by `nuncio-core`, and Rust forbids inherent impls on foreign types.
## Test plan
- [x] `to_ical_roundtrips_through_parse_ical_for_all_event_shapes` — round-trips `parse_ical(to_ical(event))` back to an equal event for: a plain timed event, an all-day (whole-UTC-day) event, an event whose UTC instant originated from a `TZID` source, and a recurring event carrying an `RRULE`.
- [x] `to_ical_escapes_rfc5545_special_characters_in_text_fields` — asserts the literal escaped `SUMMARY`/`LOCATION` output and that the escaped payload still round-trips.
- [x] `to_ical_wraps_a_single_vevent_in_a_vcalendar_shell` — asserts the `VCALENDAR`/`VERSION:2.0`/single-`VEVENT` shape.
- [x] `cargo fmt --all -- --check` — clean
- [x] `cargo check-all` (clippy `--all-targets --workspace -D warnings`) — 0 warnings
- [x] `cargo test-all` — 100% pass, 0 ignored (26 tests in `nuncio-cal`, full workspace suite green; pre-commit hook ran and passed all three gates)
Refs #310
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
ToIcal::to_ical(extension trait onnuncio_core::model::CalendarEvent, implemented incrates/nuncio-cal/src/ical.rs), the missing serializer needed for CalDAV write-back.VEVENT(UID,DTSTAMP,DTSTART,DTEND,SUMMARY,LOCATION,RRULE) wrapped in aVCALENDARshell (VERSION:2.0,PRODID), matching the shapeIcalParserAdapter::parse_icalconsumes.DTSTART/DTENDare always emitted as UTCZ-suffixedDATE-TIMEvalues:CalendarEventonly stores a resolved UTC unix timestamp (the parser already collapsesTZID/floating/Zforms down to one UTC instant), so re-parsing the emitted UTC form always reproduces the same instant regardless of the original wire form.TEXTvalues (SUMMARY,LOCATION) are escaped per RFC 5545 3.3.11: backslash, semicolon, comma, and embedded newlines (\n);RRULEis left unescaped since it is a structured value, notTEXT.CalendarEventis owned bynuncio-core, and Rust forbids inherent impls on foreign types.Test plan
to_ical_roundtrips_through_parse_ical_for_all_event_shapes— round-tripsparse_ical(to_ical(event))back to an equal event for: a plain timed event, an all-day (whole-UTC-day) event, an event whose UTC instant originated from aTZIDsource, and a recurring event carrying anRRULE.to_ical_escapes_rfc5545_special_characters_in_text_fields— asserts the literal escapedSUMMARY/LOCATIONoutput and that the escaped payload still round-trips.to_ical_wraps_a_single_vevent_in_a_vcalendar_shell— asserts theVCALENDAR/VERSION:2.0/single-VEVENTshape.cargo fmt --all -- --check— cleancargo check-all(clippy--all-targets --workspace -D warnings) — 0 warningscargo test-all— 100% pass, 0 ignored (26 tests innuncio-cal, full workspace suite green; pre-commit hook ran and passed all three gates)Refs #310