Files
DarkRoom/ui/dr-ui/src/remote.rs
T
dtourolleandClaude Opus 5 242374fd0f Let the interface hold a backend without knowing whose it is
`dr-sync` defines `RemoteBackend` and a capability model the engine adapts
to, so a second backend can be added without touching the code that uses
one. That boundary was documentation. Seven files in `dr-ui` constructed a
`NextcloudBackend` directly, ten functions took one by concrete type, and
exactly two call sites in the tree — both inside `dr-sync` itself — ever held
the trait object. A WebDAV or local-folder backend would have had a
well-written trait to implement and nowhere to go afterwards.

The change is smaller than the finding suggests, because the trait was
already right. Every method the UI has ever called on a backend — `get`,
`put`, `list`, `delete`, `create_dir`, `move_to` — was already on it, so
nothing had to be added and no behaviour moved. Ten signatures widened to
`&dyn RemoteBackend`, sixteen constructions became `remote::connect`, and
`remote.rs` is now the only file in the interface that names a connector.

`connect` returns `Result<Box<dyn RemoteBackend>, RemoteError>`. The error
type is `dr-sync`'s rather than the connector's, which is why every call site
kept its shape — the `match`, the `let Ok(..) else`, and
`.map_err(ScanFailure::local)?` all still read as they did.

One wrinkle worth recording: `&Box<dyn Trait>` does not reach `&dyn Trait` on
its own. The compiler reaches for unsizing, which wants
`Box<dyn RemoteBackend>: RemoteBackend`, and reports a confusing missing impl
rather than suggesting a deref. Twelve call sites therefore say `&*backend`,
and two say `let backend: &dyn RemoteBackend = &*backend` where a borrow is
shared across lanes.

What this does *not* do is abstract credentials. `AppCredentials` is an app
password from Login Flow v2 — a Nextcloud protocol, not a general notion of
authenticating to a remote — and seven files still name it. An OAuth token, a
bucket key pair and an app password have no useful common shape, so deciding
what an account is across backends before a second one exists would be a
confident guess. code-health.md CH-2 now records that as the remaining half,
and it should wait for the backend that forces it.

Verified: fmt clean, clippy clean at -D warnings, 2041 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:51:23 +02:00

41 lines
1.9 KiB
Rust

// TRACES: FR-NC-12
//! The one place the interface names a backend.
//!
//! `dr-sync` defines [`RemoteBackend`] and a capability model the engine adapts
//! to, so that a second backend can be added without touching the code that
//! uses one (ARCH §8.1). Until this module existed that boundary was
//! documentation: seven files in `dr-ui` constructed a `NextcloudBackend`
//! directly and ten functions took one by concrete type, so the abstraction
//! bought nothing it was designed for and a WebDAV or local-folder backend
//! would have had nowhere to go.
//!
//! Everything above this module now works through `&dyn RemoteBackend`. Adding
//! a backend is implementing the trait and changing [`connect`] — not editing
//! seven files.
//!
//! ## What is deliberately still Nextcloud-shaped
//!
//! Credentials. [`AppCredentials`] is an app password obtained through Login
//! Flow v2, which is a Nextcloud protocol rather than a general notion of
//! "how one authenticates to a remote". Abstracting it needs a decision about
//! what an account *is* across backends — an OAuth token, a bucket key pair
//! and an app password have no useful common shape — and inventing one before
//! a second backend exists would produce a wrong answer confidently. That is
//! the remaining half of this seam, and it is a design problem rather than a
//! mechanical one.
use dr_sync::{RemoteBackend, RemoteError};
use dr_sync_nextcloud::{AppCredentials, NextcloudBackend};
/// Open a connection to the configured remote.
///
/// Returns the trait object every caller should hold. The error type is
/// `dr-sync`'s rather than the connector's, so a caller handles a failure
/// without learning which backend produced it.
pub(crate) fn connect(
creds: &AppCredentials,
user_id: &str,
) -> Result<Box<dyn RemoteBackend>, RemoteError> {
Ok(Box::new(NextcloudBackend::new(creds, user_id)?))
}