feat: default value for chat client builder

This commit is contained in:
kaichaosun 2026-06-25 17:55:20 +08:00
parent 0d38dd80b7
commit c6b56ef2e7
No known key found for this signature in database
GPG Key ID: 223E0F992F4F03BF
6 changed files with 98 additions and 129 deletions

View File

@ -116,7 +116,7 @@ fn run<T: Transport>(transport: T, cli: &Cli) -> Result<()> {
match cli.registry_url.as_deref() { match cli.registry_url.as_deref() {
Some(url) => { Some(url) => {
let registry = HttpRegistry::new(url); let registry = HttpRegistry::new(url);
let (client, events) = ChatClientBuilder::new() let (client, events) = ChatClientBuilder::default()
.transport(transport) .transport(transport)
.storage_config(storage) .storage_config(storage)
.registration(registry) .registration(registry)
@ -126,7 +126,7 @@ fn run<T: Transport>(transport: T, cli: &Cli) -> Result<()> {
launch_tui(client, events, cli) launch_tui(client, events, cli)
} }
None => { None => {
let (client, events) = ChatClientBuilder::new() let (client, events) = ChatClientBuilder::default()
.transport(transport) .transport(transport)
.storage_config(storage) .storage_config(storage)
.build() .build()
@ -192,7 +192,7 @@ fn run_logos_delivery(cli: Cli) -> Result<()> {
.context("db path contains non-UTF-8 characters")? .context("db path contains non-UTF-8 characters")?
.to_string(); .to_string();
logos_chat::ChatClientBuilder::new() logos_chat::ChatClientBuilder::default()
.storage_config(logos_chat::StorageConfig::Encrypted { .storage_config(logos_chat::StorageConfig::Encrypted {
path: db_str, path: db_str,
key: "chat-cli".to_string(), key: "chat-cli".to_string(),
@ -202,7 +202,7 @@ fn run_logos_delivery(cli: Cli) -> Result<()> {
.map_err(|e| anyhow::anyhow!("{e:?}")) .map_err(|e| anyhow::anyhow!("{e:?}"))
.context("failed to open persistent client")? .context("failed to open persistent client")?
} }
None => logos_chat::ChatClientBuilder::new() None => logos_chat::ChatClientBuilder::default()
.transport(delivery) .transport(delivery)
.build() .build()
.map_err(|e| anyhow::anyhow!("{e:?}")) .map_err(|e| anyhow::anyhow!("{e:?}"))

View File

@ -6,13 +6,13 @@ fn main() {
let bus = MessageBus::default(); let bus = MessageBus::default();
let reg = EphemeralRegistry::new(); let reg = EphemeralRegistry::new();
let (mut saro, saro_events) = ChatClientBuilder::new() let (mut saro, saro_events) = ChatClientBuilder::default()
.transport(InProcessDelivery::new(bus.clone())) .transport(InProcessDelivery::new(bus.clone()))
.registration(reg.clone()) .registration(reg.clone())
.build() .build()
.unwrap(); .unwrap();
let (mut raya, raya_events) = ChatClientBuilder::new() let (mut raya, raya_events) = ChatClientBuilder::default()
.transport(InProcessDelivery::new(bus)) .transport(InProcessDelivery::new(bus))
.registration(reg) .registration(reg)
.build() .build()

View File

@ -9,8 +9,10 @@ use crate::delegate::DelegateSigner;
use crate::errors::ClientError; use crate::errors::ClientError;
use crate::event::Event; use crate::event::Event;
/// Marker for a builder field that has not been configured; the corresponding /// Marker for a builder field that has not been configured. A field left `Unset`
/// component will be filled in with a sensible default when `build()` is called. /// 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 Unset;
pub struct ChatClientBuilder<I = Unset, T = Unset, R = Unset, S = Unset> { pub struct ChatClientBuilder<I = Unset, T = Unset, R = Unset, S = Unset> {
@ -20,8 +22,16 @@ pub struct ChatClientBuilder<I = Unset, T = Unset, R = Unset, S = Unset> {
storage: S, storage: S,
} }
impl Default for ChatClientBuilder { impl ChatClientBuilder {
fn default() -> Self { /// 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 { Self {
ident: Unset, ident: Unset,
transport: Unset, transport: Unset,
@ -29,11 +39,17 @@ impl Default for ChatClientBuilder {
storage: Unset, storage: Unset,
} }
} }
}
impl ChatClientBuilder { /// A builder pre-filled with the default identity, registration, and storage.
pub fn new() -> Self { /// Only the transport is left to set; override any default with the matching
Self::default() /// 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<DelegateSigner, Unset, EphemeralRegistry, ChatStorage> {
Self::new()
.ident(DelegateSigner::random())
.registration(EphemeralRegistry::new())
.storage(ChatStorage::in_memory())
} }
} }
@ -90,7 +106,11 @@ impl<I, T, R, S> ChatClientBuilder<I, T, R, S> {
type Built<I, T, R, S> = Result<(ChatClient<I, T, R, S>, Receiver<Event>), ClientError>; type Built<I, T, R, S> = Result<(ChatClient<I, T, R, S>, Receiver<Event>), 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<I, T, R, S> ChatClientBuilder<I, T, R, S> impl<I, T, R, S> ChatClientBuilder<I, T, R, S>
where where
I: IdentityProvider + Send + 'static, I: IdentityProvider + Send + 'static,
@ -102,114 +122,3 @@ where
ChatClient::new(self.ident, self.transport, self.registration, self.storage) ChatClient::new(self.ident, self.transport, self.registration, self.storage)
} }
} }
// Transport only; I, R, S all default.
impl<T: Transport + Send + 'static> ChatClientBuilder<Unset, T, Unset, Unset> {
pub fn build(self) -> Built<DelegateSigner, T, EphemeralRegistry, ChatStorage> {
ChatClient::new(
DelegateSigner::random(),
self.transport,
EphemeralRegistry::new(),
ChatStorage::in_memory(),
)
}
}
// I and T; R and S default.
impl<I, T> ChatClientBuilder<I, T, Unset, Unset>
where
I: IdentityProvider + Send + 'static,
T: Transport + Send + 'static,
{
pub fn build(self) -> Built<I, T, EphemeralRegistry, ChatStorage> {
ChatClient::new(
self.ident,
self.transport,
EphemeralRegistry::new(),
ChatStorage::in_memory(),
)
}
}
// T and R; I and S default.
impl<T, R> ChatClientBuilder<Unset, T, R, Unset>
where
T: Transport + Send + 'static,
R: RegistrationService + Send + 'static,
{
pub fn build(self) -> Built<DelegateSigner, T, R, ChatStorage> {
ChatClient::new(
DelegateSigner::random(),
self.transport,
self.registration,
ChatStorage::in_memory(),
)
}
}
// T and S; I and R default.
impl<T, S> ChatClientBuilder<Unset, T, Unset, S>
where
T: Transport + Send + 'static,
S: ChatStore + Send + 'static,
{
pub fn build(self) -> Built<DelegateSigner, T, EphemeralRegistry, S> {
ChatClient::new(
DelegateSigner::random(),
self.transport,
EphemeralRegistry::new(),
self.storage,
)
}
}
// I, T, and R; S defaults.
impl<I, T, R> ChatClientBuilder<I, T, R, Unset>
where
I: IdentityProvider + Send + 'static,
T: Transport + Send + 'static,
R: RegistrationService + Send + 'static,
{
pub fn build(self) -> Built<I, T, R, ChatStorage> {
ChatClient::new(
self.ident,
self.transport,
self.registration,
ChatStorage::in_memory(),
)
}
}
// T, R, and S; I defaults.
impl<T, R, S> ChatClientBuilder<Unset, T, R, S>
where
T: Transport + Send + 'static,
R: RegistrationService + Send + 'static,
S: ChatStore + Send + 'static,
{
pub fn build(self) -> Built<DelegateSigner, T, R, S> {
ChatClient::new(
DelegateSigner::random(),
self.transport,
self.registration,
self.storage,
)
}
}
// I, T, and S; R defaults.
impl<I, T, S> ChatClientBuilder<I, T, Unset, S>
where
I: IdentityProvider + Send + 'static,
T: Transport + Send + 'static,
S: ChatStore + Send + 'static,
{
pub fn build(self) -> Built<I, T, EphemeralRegistry, S> {
ChatClient::new(
self.ident,
self.transport,
EphemeralRegistry::new(),
self.storage,
)
}
}

View File

@ -4,10 +4,12 @@ mod delegate;
mod delivery_in_process; mod delivery_in_process;
mod errors; mod errors;
mod event; mod event;
mod logos;
pub use builder::{ChatClientBuilder, Unset}; pub use builder::{ChatClientBuilder, Unset};
pub use client::{ChatClient, Transport}; pub use client::{ChatClient, Transport};
pub use delegate::DelegateSigner; pub use delegate::DelegateSigner;
pub use logos::LogosChatClient;
pub use delivery_in_process::{InProcessDelivery, MessageBus}; pub use delivery_in_process::{InProcessDelivery, MessageBus};
pub use errors::ClientError; pub use errors::ClientError;
pub use event::{Event, MessageSender}; pub use event::{Event, MessageSender};

View File

@ -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<T> = ChatClient<DelegateSigner, T, HttpRegistry, ChatStorage>;
impl<T> LogosChatClient<T>
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<String>,
db_key: impl Into<String>,
) -> Result<(Self, Receiver<Event>), 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()
}
}

View File

@ -39,7 +39,7 @@ fn create_test_client(
logos_chat::ClientError, logos_chat::ClientError,
> { > {
let d = InProcessDelivery::new(message_bus); let d = InProcessDelivery::new(message_bus);
ChatClientBuilder::new() ChatClientBuilder::default()
.transport(d) .transport(d)
.registration(reg) .registration(reg)
.build() .build()
@ -108,7 +108,7 @@ fn direct_v1_standalone_integration() {
// Build saro's client with its associated delegate so its outbound messages // Build saro's client with its associated delegate so its outbound messages
// carry a credential the receiver can verify against the published bundle. // 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) .ident(saro_delegate)
.transport(InProcessDelivery::new(bus.clone())) .transport(InProcessDelivery::new(bus.clone()))
.registration(reg_service.clone()) .registration(reg_service.clone())
@ -293,7 +293,7 @@ fn malformed_inbound_surfaces_as_error_event() {
let delivery = FailingDelivery::new(); let delivery = FailingDelivery::new();
let inbound_tx = delivery.inbound_sender(); let inbound_tx = delivery.inbound_sender();
let (_client, events) = ChatClientBuilder::new() let (_client, events) = ChatClientBuilder::default()
.transport(delivery) .transport(delivery)
.build() .build()
.expect("client create"); .expect("client create");