diff --git a/bin/chat-cli/src/main.rs b/bin/chat-cli/src/main.rs index 9a35b45..27aaee1 100644 --- a/bin/chat-cli/src/main.rs +++ b/bin/chat-cli/src/main.rs @@ -116,7 +116,7 @@ fn run(transport: T, cli: &Cli) -> Result<()> { match cli.registry_url.as_deref() { Some(url) => { let registry = HttpRegistry::new(url); - let (client, events) = ChatClientBuilder::new() + let (client, events) = ChatClientBuilder::default() .transport(transport) .storage_config(storage) .registration(registry) @@ -126,7 +126,7 @@ fn run(transport: T, cli: &Cli) -> Result<()> { launch_tui(client, events, cli) } None => { - let (client, events) = ChatClientBuilder::new() + let (client, events) = ChatClientBuilder::default() .transport(transport) .storage_config(storage) .build() @@ -192,7 +192,7 @@ fn run_logos_delivery(cli: Cli) -> Result<()> { .context("db path contains non-UTF-8 characters")? .to_string(); - logos_chat::ChatClientBuilder::new() + logos_chat::ChatClientBuilder::default() .storage_config(logos_chat::StorageConfig::Encrypted { path: db_str, key: "chat-cli".to_string(), @@ -202,7 +202,7 @@ fn run_logos_delivery(cli: Cli) -> Result<()> { .map_err(|e| anyhow::anyhow!("{e:?}")) .context("failed to open persistent client")? } - None => logos_chat::ChatClientBuilder::new() + None => logos_chat::ChatClientBuilder::default() .transport(delivery) .build() .map_err(|e| anyhow::anyhow!("{e:?}")) diff --git a/crates/client/examples/message-exchange/main.rs b/crates/client/examples/message-exchange/main.rs index ea755d5..2e87564 100644 --- a/crates/client/examples/message-exchange/main.rs +++ b/crates/client/examples/message-exchange/main.rs @@ -6,13 +6,13 @@ fn main() { let bus = MessageBus::default(); let reg = EphemeralRegistry::new(); - let (mut saro, saro_events) = ChatClientBuilder::new() + let (mut saro, saro_events) = ChatClientBuilder::default() .transport(InProcessDelivery::new(bus.clone())) .registration(reg.clone()) .build() .unwrap(); - let (mut raya, raya_events) = ChatClientBuilder::new() + let (mut raya, raya_events) = ChatClientBuilder::default() .transport(InProcessDelivery::new(bus)) .registration(reg) .build() diff --git a/crates/client/src/builder.rs b/crates/client/src/builder.rs index e093dfe..654e94c 100644 --- a/crates/client/src/builder.rs +++ b/crates/client/src/builder.rs @@ -9,8 +9,10 @@ use crate::delegate::DelegateSigner; use crate::errors::ClientError; use crate::event::Event; -/// Marker for a builder field that has not been configured; the corresponding -/// component will be filled in with a sensible default when `build()` is called. +/// Marker for a builder field that has not been configured. A field left `Unset` +/// at `build()` is a compile error: `build()` requires every component to have a +/// concrete type. Use [`ChatClientBuilder::default`] to start from filled-in +/// defaults instead. pub struct Unset; pub struct ChatClientBuilder { @@ -20,8 +22,16 @@ pub struct ChatClientBuilder { storage: S, } -impl Default for ChatClientBuilder { - fn default() -> Self { +impl ChatClientBuilder { + /// An empty builder: every component is `Unset` and must be supplied before + /// [`build`](ChatClientBuilder::build). For the common case, prefer + /// [`default`](ChatClientBuilder::default), which pre-fills the components. + // + // `Default` is intentionally not implemented: `default()` below is a distinct + // constructor that returns a *different*, pre-filled builder type, which the + // `Default` trait (`fn() -> Self`) cannot express. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { Self { ident: Unset, transport: Unset, @@ -29,11 +39,17 @@ impl Default for ChatClientBuilder { storage: Unset, } } -} -impl ChatClientBuilder { - pub fn new() -> Self { - Self::default() + /// A builder pre-filled with the default identity, registration, and storage. + /// Only the transport is left to set; override any default with the matching + /// setter. A complete entry point on its own — there is no need to call + /// [`new`](ChatClientBuilder::new) first. + #[allow(clippy::should_implement_trait)] + pub fn default() -> ChatClientBuilder { + Self::new() + .ident(DelegateSigner::random()) + .registration(EphemeralRegistry::new()) + .storage(ChatStorage::in_memory()) } } @@ -90,7 +106,11 @@ impl ChatClientBuilder { type Built = Result<(ChatClient, Receiver), ClientError>; -// All four explicitly provided. +/// `build()` exists only once every component has a concrete type. Any field +/// still `Unset` (always at least the transport, which has no default) fails the +/// bounds below, so an incomplete builder is a compile error rather than a +/// runtime one. Start from [`default`](ChatClientBuilder::default) to fill the +/// identity/registration/storage slots, then set the transport. impl ChatClientBuilder where I: IdentityProvider + Send + 'static, @@ -102,114 +122,3 @@ where ChatClient::new(self.ident, self.transport, self.registration, self.storage) } } - -// Transport only; I, R, S all default. -impl ChatClientBuilder { - pub fn build(self) -> Built { - ChatClient::new( - DelegateSigner::random(), - self.transport, - EphemeralRegistry::new(), - ChatStorage::in_memory(), - ) - } -} - -// I and T; R and S default. -impl ChatClientBuilder -where - I: IdentityProvider + Send + 'static, - T: Transport + Send + 'static, -{ - pub fn build(self) -> Built { - ChatClient::new( - self.ident, - self.transport, - EphemeralRegistry::new(), - ChatStorage::in_memory(), - ) - } -} - -// T and R; I and S default. -impl ChatClientBuilder -where - T: Transport + Send + 'static, - R: RegistrationService + Send + 'static, -{ - pub fn build(self) -> Built { - ChatClient::new( - DelegateSigner::random(), - self.transport, - self.registration, - ChatStorage::in_memory(), - ) - } -} - -// T and S; I and R default. -impl ChatClientBuilder -where - T: Transport + Send + 'static, - S: ChatStore + Send + 'static, -{ - pub fn build(self) -> Built { - ChatClient::new( - DelegateSigner::random(), - self.transport, - EphemeralRegistry::new(), - self.storage, - ) - } -} - -// I, T, and R; S defaults. -impl ChatClientBuilder -where - I: IdentityProvider + Send + 'static, - T: Transport + Send + 'static, - R: RegistrationService + Send + 'static, -{ - pub fn build(self) -> Built { - ChatClient::new( - self.ident, - self.transport, - self.registration, - ChatStorage::in_memory(), - ) - } -} - -// T, R, and S; I defaults. -impl ChatClientBuilder -where - T: Transport + Send + 'static, - R: RegistrationService + Send + 'static, - S: ChatStore + Send + 'static, -{ - pub fn build(self) -> Built { - ChatClient::new( - DelegateSigner::random(), - self.transport, - self.registration, - self.storage, - ) - } -} - -// I, T, and S; R defaults. -impl ChatClientBuilder -where - I: IdentityProvider + Send + 'static, - T: Transport + Send + 'static, - S: ChatStore + Send + 'static, -{ - pub fn build(self) -> Built { - ChatClient::new( - self.ident, - self.transport, - EphemeralRegistry::new(), - self.storage, - ) - } -} diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index becf852..718f70b 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -4,10 +4,12 @@ mod delegate; mod delivery_in_process; mod errors; mod event; +mod logos; pub use builder::{ChatClientBuilder, Unset}; pub use client::{ChatClient, Transport}; pub use delegate::DelegateSigner; +pub use logos::LogosChatClient; pub use delivery_in_process::{InProcessDelivery, MessageBus}; pub use errors::ClientError; pub use event::{Event, MessageSender}; diff --git a/crates/client/src/logos.rs b/crates/client/src/logos.rs new file mode 100644 index 0000000..62f1df3 --- /dev/null +++ b/crates/client/src/logos.rs @@ -0,0 +1,58 @@ +//! The opinionated Logos client. +//! +//! [`ChatClientBuilder`] is generic and can only default the zero-config +//! components (random identity, ephemeral registry, in-memory storage) — it has +//! no way to know a registry endpoint or a database path, so its defaults are +//! the test-grade ones. `LogosChatClient` is the layer that *does* commit to a +//! stack: a delegate identity, the HTTP keypackage + account registry, and +//! encrypted on-disk storage. It exists so independently built clients share the +//! same production services instead of each re-deriving them. +//! +//! Only the transport is left to the caller: it carries native dependencies and +//! environment-specific configuration that belong to the binary, not here. + +use crossbeam_channel::Receiver; +use libchat::{ChatStorage, StorageConfig}; + +use crate::ChatClientBuilder; +use crate::client::{ChatClient, Transport}; +use crate::delegate::DelegateSigner; +use crate::errors::ClientError; +use crate::event::Event; +use components::HttpRegistry; + +// The endpoint for account and keypackage registration service. +const REGISTRY_ENDPOINT: &str = "http://127.0.0.1:18080"; + +/// A [`ChatClient`] wired to the Logos service stack: a [`DelegateSigner`] +/// identity, the HTTP keypackage + account registry ([`HttpRegistry`], which is +/// both the keypackage store and the account → device directory), and encrypted +/// [`ChatStorage`]. Only the transport `T` is supplied by the caller. +pub type LogosChatClient = ChatClient; + +impl LogosChatClient +where + T: Transport + Send + 'static, +{ + /// Open a client on the Logos stack over `transport`, persisting to the + /// encrypted database at `db_path` unlocked with `db_key`. The identity and + /// registry are preconfigured; the registry endpoint is a placeholder for now. + /// + /// `db_path` is a per-client location and `db_key` is a secret, so both are + /// caller-supplied — never baked into the library. + pub fn open( + transport: T, + db_path: impl Into, + db_key: impl Into, + ) -> Result<(Self, Receiver), ClientError> { + ChatClientBuilder::new() + .ident(DelegateSigner::random()) + .transport(transport) + .registration(HttpRegistry::new(REGISTRY_ENDPOINT)) + .storage_config(StorageConfig::Encrypted { + path: db_path.into(), + key: db_key.into(), + }) + .build() + } +} diff --git a/crates/client/tests/saro_and_raya.rs b/crates/client/tests/saro_and_raya.rs index 091083c..4ebbba0 100644 --- a/crates/client/tests/saro_and_raya.rs +++ b/crates/client/tests/saro_and_raya.rs @@ -39,7 +39,7 @@ fn create_test_client( logos_chat::ClientError, > { let d = InProcessDelivery::new(message_bus); - ChatClientBuilder::new() + ChatClientBuilder::default() .transport(d) .registration(reg) .build() @@ -108,7 +108,7 @@ fn direct_v1_standalone_integration() { // Build saro's client with its associated delegate so its outbound messages // carry a credential the receiver can verify against the published bundle. - let (mut saro, _saro_events) = ChatClientBuilder::new() + let (mut saro, _saro_events) = ChatClientBuilder::default() .ident(saro_delegate) .transport(InProcessDelivery::new(bus.clone())) .registration(reg_service.clone()) @@ -293,7 +293,7 @@ fn malformed_inbound_surfaces_as_error_event() { let delivery = FailingDelivery::new(); let inbound_tx = delivery.inbound_sender(); - let (_client, events) = ChatClientBuilder::new() + let (_client, events) = ChatClientBuilder::default() .transport(delivery) .build() .expect("client create");