feat(contacts): build a real CardDAV engine, replacing the fabricated client #217

Merged
KofTwentyTwo merged 1 commits from story/m2-contacts-engine into dev 2026-07-29 22:53:55 +00:00
KofTwentyTwo commented 2026-07-29 22:50:10 +00:00 (Migrated from github.com)

Closes #187.

Summary

  • Replaces CardDavClient::fetch_remote_vcards's hardcoded "Google CardDAV Sync Contact" return with a real reqwest-based CardDAV REPORT/addressbook-query client (RFC 6352) that parses a genuine multistatus response.
  • Adds a real vCard (RFC 6350) parser (VCardParserAdapter) — the crate previously only had a generator (Contact::to_vcard) — and proves lossless round-trip for FN/N/ORG/TITLE/primary EMAIL/TEL via vcard_round_trip_preserves_core_fields.
  • Adds ContactsBackend trait + MockContactsBackend, mirroring nuncio_cal::CalendarBackend/MockCalendarBackend exactly.
  • Expands CardDavError to ParseFailed/TransportFailed, mirroring CalendarError, so a transport failure and a malformed-vCard parse failure are distinguishable typed errors — never a fabricated empty/hardcoded success.
  • Adds real, tested save_contact/list_contacts/get_contact to nuncio_store::db::DatabaseEngine, backed by a genuine contacts SQLite table + trigram FTS mirror (contacts_fts), so contacts persist through the daemon's single owned store instead of nuncio-contacts's disconnected ContactsDatabase pool.
  • Opts nuncio-contacts into the workspace lint gate: [lints]\nworkspace = true in Cargo.toml, #![forbid(unsafe_code)] in lib.rs.
  • nuncio-contacts::db::ContactsDatabase is left as-is (not deleted); it is a known, intentional dead-code gap from the daemon's perspective until the CLI's Commands::Contact handlers are rewired in M2-S2 (out of scope here, same as runner.rs:453-464's existing comment style for the analogous mail-vertical gap).

Decision: dependency direction (flagged for PO review)

The issue's Cargo.toml bullet for crates/nuncio-contacts/Cargo.toml says to add nuncio-store as a dependency there, but the issue's store-CRUD bullet says nuncio-store gains a dependency on nuncio_contacts::Contact. Both together would be a dependency cycle. I implemented the direction that is unambiguous and repeated multiple times in the issue text (nuncio-store depends on nuncio-contacts, not the reverse) and did not add nuncio-store as a dependency of nuncio-contacts. Flagging this discrepancy explicitly for review.

No proto, no nunciod, no CLI changes — engine-only, per the issue's explicit scope (M2-S2 wires this into the daemon/gRPC/CLI).

Gate evidence

cargo fmt --all -- --check   # clean, no output
cargo check-all               # clippy --all-targets --workspace -D warnings: clean, 0 warnings
cargo test-all                 # full workspace test-all: 0 failed, 0 ignored
  nuncio-contacts:  12 unit tests + 3 wiremock tests = 15 passed
  nuncio-store:     53 unit tests (incl. 3 new contact CRUD tests) = 53 passed
  workspace total:  all suites green (nuncio-cal 23+2, nuncio-cli 27+4, nuncio-contacts 12+3,
                     nuncio-core 52, nuncio-filter 24+2, nuncio-mail 36+1, nuncio-proto 19,
                     nuncio-store 53, nunciod 75+1+3+1) — 0 ignored throughout

Local pre-commit hook (fmt + clippy -D warnings + full test-all) ran and passed on the commit.

Test plan

  • cargo test -p nuncio-contacts — 15/15 passed (unit + wiremock)
  • cargo test -p nuncio-store — 53/53 passed (incl. save_get_list_and_search_contact, save_contact_upserts_existing_row_and_fts_entry, get_contact_reports_not_found_for_missing_contact)
  • cargo test-all — full workspace green, 0 ignored
  • cargo check-all — 0 clippy warnings
  • cargo fmt --all -- --check — clean
Closes #187. ## Summary - Replaces `CardDavClient::fetch_remote_vcards`'s hardcoded "Google CardDAV Sync Contact" return with a real `reqwest`-based CardDAV `REPORT`/`addressbook-query` client (RFC 6352) that parses a genuine `multistatus` response. - Adds a real vCard (RFC 6350) parser (`VCardParserAdapter`) — the crate previously only had a generator (`Contact::to_vcard`) — and proves lossless round-trip for `FN`/`N`/`ORG`/`TITLE`/primary `EMAIL`/`TEL` via `vcard_round_trip_preserves_core_fields`. - Adds `ContactsBackend` trait + `MockContactsBackend`, mirroring `nuncio_cal::CalendarBackend`/`MockCalendarBackend` exactly. - Expands `CardDavError` to `ParseFailed`/`TransportFailed`, mirroring `CalendarError`, so a transport failure and a malformed-vCard parse failure are distinguishable typed errors — never a fabricated empty/hardcoded success. - Adds real, tested `save_contact`/`list_contacts`/`get_contact` to `nuncio_store::db::DatabaseEngine`, backed by a genuine `contacts` SQLite table + trigram FTS mirror (`contacts_fts`), so contacts persist through the daemon's single owned store instead of `nuncio-contacts`'s disconnected `ContactsDatabase` pool. - Opts `nuncio-contacts` into the workspace lint gate: `[lints]\nworkspace = true` in `Cargo.toml`, `#![forbid(unsafe_code)]` in `lib.rs`. - `nuncio-contacts::db::ContactsDatabase` is left as-is (not deleted); it is a known, intentional dead-code gap from the daemon's perspective until the CLI's `Commands::Contact` handlers are rewired in **M2-S2** (out of scope here, same as `runner.rs:453-464`'s existing comment style for the analogous mail-vertical gap). ## Decision: dependency direction (flagged for PO review) The issue's Cargo.toml bullet for `crates/nuncio-contacts/Cargo.toml` says to add `nuncio-store` as a dependency there, but the issue's store-CRUD bullet says `nuncio-store` gains a dependency on `nuncio_contacts::Contact`. Both together would be a dependency cycle. I implemented the direction that is unambiguous and repeated multiple times in the issue text (`nuncio-store` depends on `nuncio-contacts`, not the reverse) and did **not** add `nuncio-store` as a dependency of `nuncio-contacts`. Flagging this discrepancy explicitly for review. No proto, no `nunciod`, no CLI changes — engine-only, per the issue's explicit scope (M2-S2 wires this into the daemon/gRPC/CLI). ## Gate evidence ``` cargo fmt --all -- --check # clean, no output cargo check-all # clippy --all-targets --workspace -D warnings: clean, 0 warnings cargo test-all # full workspace test-all: 0 failed, 0 ignored nuncio-contacts: 12 unit tests + 3 wiremock tests = 15 passed nuncio-store: 53 unit tests (incl. 3 new contact CRUD tests) = 53 passed workspace total: all suites green (nuncio-cal 23+2, nuncio-cli 27+4, nuncio-contacts 12+3, nuncio-core 52, nuncio-filter 24+2, nuncio-mail 36+1, nuncio-proto 19, nuncio-store 53, nunciod 75+1+3+1) — 0 ignored throughout ``` Local pre-commit hook (fmt + clippy `-D warnings` + full test-all) ran and passed on the commit. ## Test plan - [x] `cargo test -p nuncio-contacts` — 15/15 passed (unit + wiremock) - [x] `cargo test -p nuncio-store` — 53/53 passed (incl. `save_get_list_and_search_contact`, `save_contact_upserts_existing_row_and_fts_entry`, `get_contact_reports_not_found_for_missing_contact`) - [x] `cargo test-all` — full workspace green, 0 ignored - [x] `cargo check-all` — 0 clippy warnings - [x] `cargo fmt --all -- --check` — clean
Sign in to join this conversation.