use std::fmt;
use std::time::Duration;
use crate::cache::error::CacheConfigError;
use crate::cache::namespace::Namespace;
#[derive(Clone)]
pub struct CacheConfig {
client: redis::Client,
host: String,
port: u16,
database: u8,
response_timeout: Duration,
max_payload_size: Option<usize>,
namespace: Namespace,
}
impl CacheConfig {
pub fn new(url: &str) -> Result<Self, CacheConfigError> {
let client = redis::Client::open(url).map_err(|error| CacheConfigError::InvalidUrl {
detail: error.to_string(),
})?;
let (host, port, database) = extract_connection_metadata(&client);
Ok(Self {
client,
host,
port,
database,
response_timeout: Duration::from_secs(5),
max_payload_size: Some(64 * 1024 * 1024),
namespace: Namespace::none(),
})
}
#[must_use]
pub fn response_timeout(mut self, timeout: Duration) -> Self {
self.response_timeout = timeout;
self
}
#[must_use]
pub fn max_payload_size(mut self, limit: Option<usize>) -> Self {
self.max_payload_size = limit;
self
}
#[must_use]
pub fn namespace(mut self, namespace: Namespace) -> Self {
self.namespace = namespace;
self
}
pub(crate) fn client(&self) -> &redis::Client {
&self.client
}
pub(crate) fn response_timeout_setting(&self) -> Duration {
self.response_timeout
}
pub(crate) fn max_payload_size_setting(&self) -> Option<usize> {
self.max_payload_size
}
pub(crate) fn namespace_setting(&self) -> &Namespace {
&self.namespace
}
pub(crate) fn validate(&self) -> Result<(), CacheConfigError> {
if self.response_timeout.is_zero() {
return Err(CacheConfigError::ZeroResponseTimeout);
}
if let Some(limit) = self.max_payload_size
&& limit == 0
{
return Err(CacheConfigError::ZeroPayloadLimit);
}
Ok(())
}
}
impl fmt::Debug for CacheConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CacheConfig")
.field("host", &self.host)
.field("port", &self.port)
.field("database", &self.database)
.field("response_timeout", &self.response_timeout)
.field("max_payload_size", &self.max_payload_size)
.field("namespace", &self.namespace)
.finish()
}
}
impl fmt::Display for CacheConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}:{}/{}", self.host, self.port, self.database)
}
}
fn extract_connection_metadata(client: &redis::Client) -> (String, u16, u8) {
let info = client.get_connection_info();
let (host, port) = match info.addr() {
redis::ConnectionAddr::Tcp(host, port) => (host.clone(), *port),
redis::ConnectionAddr::TcpTls { host, port, .. } => (host.clone(), *port),
redis::ConnectionAddr::Unix(path) => (path.display().to_string(), 0),
_ => (String::new(), 0),
};
let database = info.redis_settings().db().max(0) as u8;
(host, port, database)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_valid_url() {
let config = CacheConfig::new("redis://localhost:6379/0").expect("valid URL");
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 6379);
assert_eq!(config.database, 0);
}
#[test]
fn parses_url_with_password() {
let config = CacheConfig::new("redis://:secret@localhost:6379/1").expect("valid URL");
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 6379);
assert_eq!(config.database, 1);
}
#[test]
fn debug_does_not_expose_credentials() {
let config = CacheConfig::new("redis://:secret_pass@localhost:6379/0").expect("valid URL");
let debug_output = format!("{config:?}");
assert!(
!debug_output.contains("secret_pass"),
"debug leaked password: {debug_output}"
);
assert!(debug_output.contains("localhost"));
assert!(debug_output.contains("6379"));
}
#[test]
fn display_does_not_expose_credentials() {
let config = CacheConfig::new("redis://:secret_pass@localhost:6379/0").expect("valid URL");
let display_output = format!("{config}");
assert!(!display_output.contains("secret_pass"));
assert!(display_output.contains("localhost:6379/0"));
}
#[test]
fn defaults_are_sensible() {
let config = CacheConfig::new("redis://localhost:6379/0").expect("valid URL");
assert_eq!(config.response_timeout_setting(), Duration::from_secs(5));
assert_eq!(config.max_payload_size_setting(), Some(64 * 1024 * 1024));
}
#[test]
fn overrides_apply() {
let config = CacheConfig::new("redis://localhost:6379/0")
.expect("valid URL")
.response_timeout(Duration::from_secs(30))
.max_payload_size(Some(1024));
assert_eq!(config.response_timeout_setting(), Duration::from_secs(30));
assert_eq!(config.max_payload_size_setting(), Some(1024));
}
}