feat(calendar): bounded caldav report calendar-query and timezone expansion #42

Merged
yuris merged 1 commit from feat/issue-8-caldav-query into main 2026-09-22 19:18:48 +00:00
Owner

Motivation

Issue #8 requires bounded CalDAV REPORT calendar-query as the authoritative recurrence engine: server-side expansion via <C:expand>, strict half-open [from, to) interval math in a mandatory configured IANA timezone, tagged date/instant representations, and fail-fast enforcement of the 93-day span / 10,000-instance ceilings. An unreachable Radicale must yield dependency_unavailable, never an empty agenda.

Changes

  • src/calendar/timezone.rs:
    • CalendarTimezone wrapping chrono_tz::Tz, parsed strictly from config; invalid IANA strings fail with configuration_invalid, no host-timezone fallback.
    • period_today / period_tomorrow / period_week build local-midnight (week: Monday 00:00) boundaries in local calendar time, then convert to UTC; half-open [from, to) throughout.
    • HalfOpenInterval::new enforces start < end and the 93-day (MAX_QUERY_SPAN_DAYS) span ceiling, returning limit_exceeded with a narrow-range instruction.
    • TaggedTime (date / instant serde-tagged), DST fold/gap handling via resolve_local_datetime: ambiguous fold without an explicit offset or DstChoice returns clarification_required; nonexistent gap times are rejected with invalid_input, never auto-shifted.
  • src/calendar/report.rs:
    • build_calendar_query_xml: RFC 4791 calendar-query envelope with <D:getetag/>, <C:calendar-data><C:expand start=... end=.../></C:calendar-data>, and VCALENDAR > VEVENT/VTODO > time-range filters.
    • parse_report_multistatus: namespace-aware quick-xml parsing of 207 Multi-Status (href, getetag, calendar-data, per-propstat status), with byte-size and depth caps.
    • parse_and_expand_report: RFC 5545 line unfolding, parameter parsing, and normalization into ExpandedComponent (UID, SUMMARY, STATUS, TRANSP, RECURRENCE-ID with TZID identity, DTSTART/DTEND/DUE). All-day VALUE=DATE times keep DTEND exclusive, defaulting a missing DTEND to start+1 day (Scenario C04). Floating datetimes are interpreted in the configured timezone; UTC/TZID datetimes stay tagged instants. Instance ceiling of 10,000 (MAX_EXPANDED_INSTANCES) aborts with limit_exceeded.
    • execute_calendar_query: dispatches HTTP REPORT (Depth: 1, application/xml) through CalDavClient and normalizes the result.
  • src/calendar/client.rs: added report() next to propfind(); transport errors on connect/timeout map to dependency_unavailable (distinct from a successful empty response), 502/503/504 map to dependency_unavailable, auth failures to forbidden.
  • src/calendar/mod.rs: error-code/exit-code mappings for limit_exceeded, dependency_unavailable, clarification_required, configuration_invalid.
  • tests/caldav_report_tests.rs: coverage for W03 period boundaries, C03 DST fold/gap, C04 all-day exclusivity, C08 span + instance ceilings, XML builder shape, multistatus parsing with detached overrides, a full in-process Axum REPORT flow, and C01 empty-agenda-vs-unreachable.

Verification

  • nix develop --command cargo test — all suites pass, including 10 new tests in tests/caldav_report_tests.rs (W03, C01, C03, C04, C08 covered).
  • cargo clippy --all-targets -- -D warnings — clean.
  • cargo fmt --check — clean.

Closes #8. Builds on #7 (CalDAV connection pooling, PROPFIND discovery).

## Motivation Issue #8 requires bounded CalDAV `REPORT` `calendar-query` as the authoritative recurrence engine: server-side expansion via `<C:expand>`, strict half-open `[from, to)` interval math in a mandatory configured IANA timezone, tagged date/instant representations, and fail-fast enforcement of the 93-day span / 10,000-instance ceilings. An unreachable Radicale must yield `dependency_unavailable`, never an empty agenda. ## Changes - `src/calendar/timezone.rs`: - `CalendarTimezone` wrapping `chrono_tz::Tz`, parsed strictly from config; invalid IANA strings fail with `configuration_invalid`, no host-timezone fallback. - `period_today` / `period_tomorrow` / `period_week` build local-midnight (week: Monday 00:00) boundaries in local calendar time, then convert to UTC; half-open `[from, to)` throughout. - `HalfOpenInterval::new` enforces `start < end` and the 93-day (`MAX_QUERY_SPAN_DAYS`) span ceiling, returning `limit_exceeded` with a narrow-range instruction. - `TaggedTime` (`date` / `instant` serde-tagged), DST fold/gap handling via `resolve_local_datetime`: ambiguous fold without an explicit offset or `DstChoice` returns `clarification_required`; nonexistent gap times are rejected with `invalid_input`, never auto-shifted. - `src/calendar/report.rs`: - `build_calendar_query_xml`: RFC 4791 `calendar-query` envelope with `<D:getetag/>`, `<C:calendar-data><C:expand start=... end=.../></C:calendar-data>`, and `VCALENDAR > VEVENT/VTODO > time-range` filters. - `parse_report_multistatus`: namespace-aware `quick-xml` parsing of `207 Multi-Status` (href, getetag, calendar-data, per-propstat status), with byte-size and depth caps. - `parse_and_expand_report`: RFC 5545 line unfolding, parameter parsing, and normalization into `ExpandedComponent` (UID, SUMMARY, STATUS, TRANSP, RECURRENCE-ID with TZID identity, DTSTART/DTEND/DUE). All-day `VALUE=DATE` times keep `DTEND` exclusive, defaulting a missing DTEND to start+1 day (Scenario C04). Floating datetimes are interpreted in the configured timezone; UTC/TZID datetimes stay tagged instants. Instance ceiling of 10,000 (`MAX_EXPANDED_INSTANCES`) aborts with `limit_exceeded`. - `execute_calendar_query`: dispatches HTTP `REPORT` (Depth: 1, `application/xml`) through `CalDavClient` and normalizes the result. - `src/calendar/client.rs`: added `report()` next to `propfind()`; transport errors on connect/timeout map to `dependency_unavailable` (distinct from a successful empty response), 502/503/504 map to `dependency_unavailable`, auth failures to `forbidden`. - `src/calendar/mod.rs`: error-code/exit-code mappings for `limit_exceeded`, `dependency_unavailable`, `clarification_required`, `configuration_invalid`. - `tests/caldav_report_tests.rs`: coverage for W03 period boundaries, C03 DST fold/gap, C04 all-day exclusivity, C08 span + instance ceilings, XML builder shape, multistatus parsing with detached overrides, a full in-process Axum REPORT flow, and C01 empty-agenda-vs-unreachable. ## Verification - `nix develop --command cargo test` — all suites pass, including 10 new tests in `tests/caldav_report_tests.rs` (W03, C01, C03, C04, C08 covered). - `cargo clippy --all-targets -- -D warnings` — clean. - `cargo fmt --check` — clean. ## Related Issues Closes #8. Builds on #7 (CalDAV connection pooling, PROPFIND discovery).
yuris merged commit 0ff6557e2d into main 2026-09-22 19:18:48 +00:00
yuris changed title from feat(calendar): bounded caldav report calendar-query and timezone expansion (#8) to feat(calendar): bounded caldav report calendar-query and timezone expansion 2026-09-23 05:12:09 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/life-service!42
No description provided.