use crate::core::errors::DataProfilerError;
use std::collections::HashMap;
use std::env;
#[derive(Clone, Default)]
pub struct DatabaseCredentials {
pub username: Option<String>,
pub password: Option<String>,
pub host: Option<String>,
pub port: Option<u16>,
pub database: Option<String>,
pub extra_params: HashMap<String, String>,
}
impl std::fmt::Debug for DatabaseCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let redacted_password: Option<&'static str> = match self.password {
Some(_) => Some("<REDACTED>"),
None => None,
};
f.debug_struct("DatabaseCredentials")
.field("username", &self.username)
.field("password", &redacted_password)
.field("host", &self.host)
.field("port", &self.port)
.field("database", &self.database)
.field("extra_params", &self.extra_params)
.finish()
}
}
impl DatabaseCredentials {
pub fn new() -> Self {
Self::default()
}
pub fn from_environment(database_type: &str) -> Self {
let prefix = match database_type {
"postgresql" => "POSTGRES",
"mysql" => "MYSQL",
"sqlite" => "SQLITE",
_ => "DATABASE",
};
let mut creds = Self::new();
creds.username = env::var(format!("{}_USER", prefix))
.ok()
.or_else(|| env::var(format!("{}_USERNAME", prefix)).ok())
.or_else(|| env::var("DATABASE_USER").ok())
.or_else(|| env::var("DB_USER").ok());
creds.password = env::var(format!("{}_PASSWORD", prefix))
.ok()
.or_else(|| env::var("DATABASE_PASSWORD").ok())
.or_else(|| env::var("DB_PASSWORD").ok());
creds.host = env::var(format!("{}_HOST", prefix))
.ok()
.or_else(|| env::var("DATABASE_HOST").ok())
.or_else(|| env::var("DB_HOST").ok());
if let Ok(port_str) = env::var(format!("{}_PORT", prefix))
.or_else(|_| env::var("DATABASE_PORT"))
.or_else(|_| env::var("DB_PORT"))
{
creds.port = port_str.parse().ok();
}
creds.database = env::var(format!("{}_DATABASE", prefix))
.ok()
.or_else(|| env::var(format!("{}_DB", prefix)).ok())
.or_else(|| env::var("DATABASE_NAME").ok())
.or_else(|| env::var("DB_NAME").ok());
if let Ok(database_url) = env::var("DATABASE_URL")
&& let Ok(parsed) = crate::database::connection::ConnectionInfo::parse(&database_url)
{
creds.username = creds.username.or(parsed.username);
creds.password = creds.password.or(parsed.password);
creds.host = creds.host.or(parsed.host);
creds.port = creds.port.or(parsed.port);
creds.database = creds.database.or(parsed.database);
}
let url_var = format!("{}_URL", prefix);
if let Ok(url) = env::var(&url_var)
&& let Ok(parsed) = crate::database::connection::ConnectionInfo::parse(&url)
{
creds.username = creds.username.or(parsed.username);
creds.password = creds.password.or(parsed.password);
creds.host = creds.host.or(parsed.host);
creds.port = creds.port.or(parsed.port);
creds.database = creds.database.or(parsed.database);
}
creds
}
pub fn apply_to_connection_string(&self, connection_string: &str) -> String {
if let Ok(mut conn_info) =
crate::database::connection::ConnectionInfo::parse(connection_string)
{
if let Some(username) = &self.username {
conn_info.username = Some(username.clone());
}
if let Some(password) = &self.password {
conn_info.password = Some(password.clone());
}
if let Some(host) = &self.host {
conn_info.host = Some(host.clone());
}
if let Some(port) = self.port {
conn_info.port = Some(port);
}
if let Some(database) = &self.database {
conn_info.database = Some(database.clone());
}
for (key, value) in &self.extra_params {
conn_info.query_params.insert(key.clone(), value.clone());
}
conn_info.to_original_string()
} else {
connection_string.to_string()
}
}
pub fn validate(&self, database_type: &str) -> Result<(), DataProfilerError> {
match database_type {
"postgresql" | "mysql" => {
if self.host.is_none() {
return Err(DataProfilerError::database_config(&format!(
"Database host is required for {}",
database_type
)));
}
if self.username.is_none() {
return Err(DataProfilerError::database_config(&format!(
"Database username is required for {}",
database_type
)));
}
}
"sqlite" => {
if self.database.is_none() {
log::warn!("No database file path specified for SQLite");
}
}
_ => {
log::warn!(
"Credential validation for database type '{}' not implemented",
database_type
);
}
}
Ok(())
}
pub fn to_masked_string(&self) -> String {
format!(
"DatabaseCredentials {{ username: {:?}, password: {}, host: {:?}, port: {:?}, database: {:?} }}",
self.username,
if self.password.is_some() {
"***"
} else {
"None"
},
self.host,
self.port,
self.database
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_credentials_masking() {
let mut creds = DatabaseCredentials::new();
creds.username = Some("testuser".to_string());
let masked = creds.to_masked_string();
assert!(masked.contains("testuser"));
let test_creds_with_pass = DatabaseCredentials {
username: Some("user".to_string()),
password: Some(format!("{}123", "testpass")), host: None,
port: None,
database: Some("testdb".to_string()),
extra_params: HashMap::new(),
};
let masked_with_pass = test_creds_with_pass.to_masked_string();
assert!(masked_with_pass.contains("***"));
assert!(!masked_with_pass.contains("testpass123"));
}
}