Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor/skills/review-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ gh pr diff "$PR" --name-only
| **Connectors** | Follows `docs/adding-a-connector.md`: `Connector` trait, resumable backfill cursor, incremental sync until `CancellationToken` fires, ingest `eprintln!` format `[connector:connection_id] …` | Missing sync contract pieces, ad-hoc persistence outside the store dir, divergent module layout (`auth`, `sync`, `api`, `models`) |
| **Module layout** | Domain-named modules; large files split into submodules; no `utils.rs`/`helpers.rs` catch-alls | New catch-all modules, logic placed in the wrong crate or layer |
| **Async & sync** | `tokio` + `CancellationToken` for long-running work; no blocking I/O in async paths | Blocking calls in async without justification; sync loops that ignore cancellation |
| **Config & models** | Extend existing `ConnectorType`, `ConnectionSettings`, serde patterns in `void-core` | One-off config structs, stringly-typed enums, schema changes without migration tests |
| **Config & models** | Registry-driven connections: `ConnectorType` string newtype, per-connection `toml::Table` settings, `connectors/<name>.rs` plugin descriptors | Central `ConnectorType` enum, `ConnectionSettings` variants, or schema changes without migration tests |
| **Database** | Access via `Database` methods in `void-core/src/db/`; migrations tested for data preservation | Raw SQL or schema logic outside `db/`; migration without a preservation test |
| **HTTP clients** | Test constructor like `with_base_url(...)`; production client separate from parsing helpers | Real network in unit tests; parsing mixed into transport layer |
| **CLI output** | Existing formatters and JSON envelope shapes; read-path changes may need `insta` snapshots | Ad-hoc printing, breaking JSON contract without snapshot update |
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Internal** — Connector wiring uses a compile-time plugin registry (`inventory`). `ConnectorType` is a string newtype; connection settings are a generic TOML table. Adding a connector no longer edits ~13 central files. Connection settings are validated at setup reload, sync start, and `void doctor`. Poll intervals are read from `[sync]` via the generic `{id}_poll_interval_secs` keys. No user-facing CLI behavior change.
- **Internal** — Expanded connector registry tests (validation, build, badges, aliases, debug redaction, poll defaults).
- **Build** — Bump minimum supported Rust version to 1.95 (required by sysinfo 0.39).

### Added
Expand Down
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ url = "2"
urlencoding = "2"
dirs = "6"
sysinfo = "0.39.3"
inventory = "0.3"

# Testing
wiremock = "0.6"
Expand Down
13 changes: 8 additions & 5 deletions crates/void-calendar/src/connector/connector_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ use void_core::connector::Connector;
use void_core::db::Database;
use void_core::models::*;

use crate::CONNECTOR_ID;

use super::types::CalendarConnector;
use crate::api::CalendarApiClient;

#[async_trait]
impl Connector for CalendarConnector {
fn connector_type(&self) -> ConnectorType {
ConnectorType::Calendar
ConnectorType::from_static(CONNECTOR_ID)
}

fn connection_id(&self) -> &str {
Expand Down Expand Up @@ -46,7 +48,8 @@ impl Connector for CalendarConnector {
async fn start_sync(&self, db: Arc<Database>, cancel: CancellationToken) -> anyhow::Result<()> {
self.initial_sync(&db).await?;

let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
let mut interval =
tokio::time::interval(std::time::Duration::from_secs(self.poll_interval_secs));
loop {
tokio::select! {
_ = cancel.cancelled() => {
Expand All @@ -70,7 +73,7 @@ impl Connector for CalendarConnector {
let count = cals.items.as_ref().map(|i| i.len()).unwrap_or(0);
Ok(HealthStatus {
connection_id: self.connection_id.clone(),
connector_type: ConnectorType::Calendar,
connector_type: ConnectorType::from_static(CONNECTOR_ID),
ok: true,
message: format!("{count} calendar(s) accessible"),
last_sync: None,
Expand All @@ -81,7 +84,7 @@ impl Connector for CalendarConnector {
warn!(connection_id = %self.connection_id, error = %e, "Calendar health check API error");
Ok(HealthStatus {
connection_id: self.connection_id.clone(),
connector_type: ConnectorType::Calendar,
connector_type: ConnectorType::from_static(CONNECTOR_ID),
ok: false,
message: format!("API error: {e}"),
last_sync: None,
Expand All @@ -93,7 +96,7 @@ impl Connector for CalendarConnector {
warn!(connection_id = %self.connection_id, error = %e, "Calendar health check auth error");
Ok(HealthStatus {
connection_id: self.connection_id.clone(),
connector_type: ConnectorType::Calendar,
connector_type: ConnectorType::from_static(CONNECTOR_ID),
ok: false,
message: format!("Auth error: {e}"),
last_sync: None,
Expand Down
3 changes: 3 additions & 0 deletions crates/void-calendar/src/connector/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub struct CalendarConnector {
pub(crate) credentials_file: Option<String>,
pub(crate) calendar_ids: Vec<String>,
pub(crate) store_path: std::path::PathBuf,
pub(crate) poll_interval_secs: u64,
}

impl CalendarConnector {
Expand All @@ -34,6 +35,7 @@ impl CalendarConnector {
credentials_file: Option<&str>,
calendar_ids: Vec<String>,
store_path: &std::path::Path,
poll_interval_secs: u64,
) -> Self {
Self {
connection_id: connection_id.to_string(),
Expand All @@ -44,6 +46,7 @@ impl CalendarConnector {
calendar_ids
},
store_path: store_path.to_path_buf(),
poll_interval_secs,
}
}
}
2 changes: 2 additions & 0 deletions crates/void-calendar/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod api;
pub mod connector;
pub mod error;

pub const CONNECTOR_ID: &str = "calendar";
2 changes: 2 additions & 0 deletions crates/void-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ libc = { workspace = true }
croner = { workspace = true }
sysinfo = { workspace = true }
uuid = { workspace = true }
inventory = { workspace = true }
toml = { workspace = true }

[dev-dependencies]
assert_cmd = { workspace = true }
Expand Down
7 changes: 5 additions & 2 deletions crates/void-cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::{Parser, Subcommand};
use clap::{CommandFactory, FromArgMatches, Parser, Subcommand};

use crate::commands;
use crate::context;
Expand Down Expand Up @@ -109,7 +109,10 @@ fn refresh_policy_for_cli(cli: &Cli) -> void_core::store::RefreshPolicy {
}

pub fn run() -> anyhow::Result<()> {
let cli = Cli::parse();
let mut cmd = Cli::command();
crate::output::patch_connector_arg_help(&mut cmd);
let matches = cmd.get_matches();
let cli = Cli::from_arg_matches(&matches)?;

let refresh = refresh_policy_for_cli(&cli);
context::init(
Expand Down
Loading