use std::time::Duration;
use crate::error::{Error, Result};
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
#[derive(Clone, PartialEq, Eq)]
enum Source {
Parts {
host: String,
port: u16,
database: Option<String>,
username: String,
password: String,
trust_cert: bool,
application_name: Option<String>,
connect_timeout: Option<Duration>,
},
Ado(String),
Jdbc(String),
}
#[derive(Clone, PartialEq, Eq)]
pub struct Config {
source: Source,
}
impl Default for Config {
fn default() -> Self {
Self::new()
}
}
impl Config {
pub fn new() -> Self {
Self {
source: Source::Parts {
host: "localhost".to_string(),
port: 1433,
database: None,
username: String::new(),
password: String::new(),
trust_cert: false,
application_name: None,
connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
},
}
}
pub fn from_ado_string(s: impl Into<String>) -> Self {
Self {
source: Source::Ado(s.into()),
}
}
pub fn from_jdbc_string(s: impl Into<String>) -> Self {
Self {
source: Source::Jdbc(s.into()),
}
}
pub fn host(mut self, host: impl Into<String>) -> Self {
if let Source::Parts { host: h, .. } = &mut self.source {
*h = host.into();
}
self
}
pub fn port(mut self, port: u16) -> Self {
if let Source::Parts { port: p, .. } = &mut self.source {
*p = port;
}
self
}
pub fn database(mut self, database: impl Into<String>) -> Self {
if let Source::Parts { database: d, .. } = &mut self.source {
*d = Some(database.into());
}
self
}
pub fn auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
if let Source::Parts {
username: u,
password: p,
..
} = &mut self.source
{
*u = username.into();
*p = password.into();
}
self
}
pub fn trust_cert(mut self) -> Self {
if let Source::Parts { trust_cert: t, .. } = &mut self.source {
*t = true;
}
self
}
pub fn connect_timeout(mut self, timeout: Option<Duration>) -> Self {
if let Source::Parts {
connect_timeout: t, ..
} = &mut self.source
{
*t = timeout;
}
self
}
pub fn application_name(mut self, name: impl Into<String>) -> Self {
if let Source::Parts {
application_name: a,
..
} = &mut self.source
{
*a = Some(name.into());
}
self
}
pub(crate) fn to_driver(&self) -> Result<tds_driver::Config> {
match &self.source {
Source::Ado(s) => tds_driver::Config::from_ado_string(s)
.map_err(|e| Error::Config(format!("invalid ADO connection string: {e}"))),
Source::Jdbc(s) => tds_driver::Config::from_jdbc_string(s)
.map_err(|e| Error::Config(format!("invalid JDBC connection string: {e}"))),
Source::Parts {
host,
port,
database,
username,
password,
trust_cert,
application_name,
connect_timeout,
} => {
let mut cfg = tds_driver::Config::new();
cfg.set_connect_timeout(*connect_timeout);
cfg.host(host);
cfg.port(*port);
if let Some(db) = database {
cfg.database(db);
}
cfg.authentication(tds_driver::AuthMethod::sql_server(username, password));
if *trust_cert {
cfg.trust_cert();
}
if let Some(name) = application_name {
cfg.application_name(name);
}
Ok(cfg)
}
}
}
}
impl std::fmt::Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.source {
Source::Ado(_) => f
.debug_struct("Config")
.field("source", &"ado_string(<redacted>)")
.finish(),
Source::Jdbc(_) => f
.debug_struct("Config")
.field("source", &"jdbc_string(<redacted>)")
.finish(),
Source::Parts {
host,
port,
database,
username,
trust_cert,
application_name,
..
} => f
.debug_struct("Config")
.field("host", host)
.field("port", port)
.field("database", database)
.field("username", username)
.field("password", &"<redacted>")
.field("trust_cert", trust_cert)
.field("application_name", application_name)
.finish(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_sets_fields() {
let cfg = Config::new()
.host("db.example.com")
.port(1444)
.database("sales")
.auth("sa", "hunter2")
.trust_cert();
let Source::Parts {
host,
port,
database,
username,
trust_cert,
..
} = &cfg.source
else {
panic!("expected parts");
};
assert_eq!(host, "db.example.com");
assert_eq!(*port, 1444);
assert_eq!(database.as_deref(), Some("sales"));
assert_eq!(username, "sa");
assert!(trust_cert);
}
#[test]
fn defaults_to_localhost() {
let cfg = Config::new();
let Source::Parts { host, port, .. } = &cfg.source else {
panic!("expected parts");
};
assert_eq!(host, "localhost");
assert_eq!(*port, 1433);
}
#[test]
fn debug_redacts_password() {
let cfg = Config::new().auth("sa", "hunter2");
let rendered = format!("{cfg:?}");
assert!(!rendered.contains("hunter2"), "password leaked: {rendered}");
assert!(rendered.contains("<redacted>"));
}
#[test]
fn ado_string_debug_is_redacted() {
let cfg = Config::from_ado_string("Server=x;Password=hunter2;");
let rendered = format!("{cfg:?}");
assert!(!rendered.contains("hunter2"), "password leaked: {rendered}");
}
#[test]
fn builds_driver_config_from_parts() {
let cfg = Config::new()
.host("localhost")
.auth("sa", "pw")
.trust_cert();
assert!(cfg.to_driver().is_ok());
}
#[test]
fn rejects_malformed_ado_string() {
let cfg = Config::from_ado_string("this is not a connection string");
assert!(matches!(cfg.to_driver(), Err(Error::Config(_))));
}
}