use std::time::Duration;
use nodedb_types::error::{NodeDbError, NodeDbResult};
use nodedb_types::protocol::AuthMethod;
use super::client::NativeClient;
use super::connection::TlsConfig;
use super::pool::PoolConfig;
#[derive(Debug, Default)]
pub struct ConnectionBuilder {
addr: Option<String>,
username: Option<String>,
password: Option<String>,
api_key: Option<String>,
database: Option<String>,
max_connections: Option<usize>,
connect_timeout: Option<Duration>,
idle_timeout: Option<Duration>,
tls: Option<TlsConfig>,
}
impl ConnectionBuilder {
pub fn new(addr: impl Into<String>) -> Self {
Self {
addr: Some(addr.into()),
..Default::default()
}
}
pub fn username(mut self, username: impl Into<String>) -> Self {
self.username = Some(username.into());
self
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
pub fn api_key(mut self, token: impl Into<String>) -> Self {
self.api_key = Some(token.into());
self
}
pub fn database(mut self, name: impl Into<String>) -> Self {
self.database = Some(name.into());
self
}
pub fn max_connections(mut self, n: usize) -> Self {
self.max_connections = Some(n);
self
}
pub fn connect_timeout(mut self, d: Duration) -> Self {
self.connect_timeout = Some(d);
self
}
pub fn idle_timeout(mut self, d: Duration) -> Self {
self.idle_timeout = Some(d);
self
}
pub fn tls(mut self, tls: TlsConfig) -> Self {
self.tls = Some(tls);
self
}
pub fn build(self) -> NodeDbResult<NativeClient> {
let addr = self.addr.unwrap_or_else(|| "127.0.0.1:6433".to_string());
let auth = resolve_auth(self.username, self.password, self.api_key)?;
let default_config = PoolConfig::new(addr, auth);
let config = PoolConfig {
database: self.database,
max_size: self.max_connections.unwrap_or(default_config.max_size),
connect_timeout: self
.connect_timeout
.unwrap_or(default_config.connect_timeout),
idle_timeout: self.idle_timeout.unwrap_or(default_config.idle_timeout),
tls: self.tls.unwrap_or_default(),
..default_config
};
Ok(NativeClient::new(config))
}
}
fn resolve_auth(
username: Option<String>,
password: Option<String>,
api_key: Option<String>,
) -> NodeDbResult<AuthMethod> {
if let Some(token) = api_key {
return Ok(AuthMethod::ApiKey { token });
}
let username = username.ok_or_else(|| {
NodeDbError::config(
"no authentication identity configured: call .username(...) or .api_key(...)",
)
})?;
Ok(match password {
Some(password) => AuthMethod::Password { username, password },
None => AuthMethod::Trust { username },
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_auth_with_username_carries_it_into_trust() {
let auth = resolve_auth(Some("alice".to_string()), None, None).expect("username given");
match auth {
AuthMethod::Trust { username } => assert_eq!(username, "alice"),
other => panic!("expected AuthMethod::Trust, got {other:?}"),
}
}
#[test]
fn resolve_auth_with_username_and_password_carries_it_into_password() {
let auth = resolve_auth(Some("bob".to_string()), Some("secret".to_string()), None)
.expect("username given");
match auth {
AuthMethod::Password { username, password } => {
assert_eq!(username, "bob");
assert_eq!(password, "secret");
}
other => panic!("expected AuthMethod::Password, got {other:?}"),
}
}
#[test]
fn resolve_auth_with_api_key_succeeds_without_username() {
let auth = resolve_auth(None, None, Some("token-123".to_string())).expect("api_key given");
assert!(matches!(auth, AuthMethod::ApiKey { token } if token == "token-123"));
}
#[test]
fn resolve_auth_with_no_identity_errors() {
let err = resolve_auth(None, None, None).expect_err("must reject a missing identity");
assert!(err.message().contains("authentication identity"));
}
#[test]
fn resolve_auth_with_password_but_no_username_errors() {
let err = resolve_auth(None, Some("secret".to_string()), None)
.expect_err("must reject password auth without a username");
assert!(err.message().contains("authentication identity"));
}
#[test]
fn builder_with_username_succeeds() {
let client = ConnectionBuilder::new("127.0.0.1:6433")
.username("alice")
.build();
assert!(client.is_ok());
}
#[test]
fn builder_with_api_key_succeeds_without_username() {
let client = ConnectionBuilder::new("127.0.0.1:6433")
.api_key("token-123")
.build();
assert!(client.is_ok());
}
#[test]
fn builder_with_no_identity_errors() {
let err = ConnectionBuilder::new("127.0.0.1:6433")
.build()
.map(|_| ())
.expect_err("build() must reject a missing identity");
assert!(err.message().contains("authentication identity"));
}
#[test]
fn builder_password_without_username_errors() {
let err = ConnectionBuilder::new("127.0.0.1:6433")
.password("secret")
.build()
.map(|_| ())
.expect_err("build() must reject password auth without a username");
assert!(err.message().contains("authentication identity"));
}
#[test]
fn builder_with_database() {
let _client = ConnectionBuilder::new("127.0.0.1:6433")
.username("alice")
.database("analytics")
.build()
.expect("username was supplied, build() must succeed");
}
#[test]
fn builder_password_auth() {
let _client = ConnectionBuilder::new("127.0.0.1:6433")
.username("bob")
.password("secret")
.database("prod")
.build()
.expect("username was supplied, build() must succeed");
}
}