use std::time::Duration; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RedisMode { #[default] Standalone, Sentinel, Cluster, ValkeyStandalone, } impl RedisMode { pub fn parse(value: &str) -> Option { if value.eq_ignore_ascii_case("standalone") { Some(Self::Standalone) } else if value.eq_ignore_ascii_case("sentinel") { Some(Self::Sentinel) } else if value.eq_ignore_ascii_case("cluster") { Some(Self::Cluster) } else if value.eq_ignore_ascii_case("valkey_standalone") || value.eq_ignore_ascii_case("valkey") { Some(Self::ValkeyStandalone) } else { None } } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default)] pub struct RedisConfig { pub url: Option, pub mode: Option, pub database: Option, pub pool_size: Option, pub connection_timeout_secs: Option, pub username: Option, pub password: Option, } impl RedisConfig { pub fn new(url: impl Into) -> Self { Self { url: Some(url.into()), ..Self::default() } } pub fn url(&self) -> &str { self.url.as_deref().unwrap_or("") } pub fn mode(&self) -> RedisMode { self.mode.unwrap_or_default() } pub fn pool_size(&self) -> usize { self.pool_size.unwrap_or(8) } pub fn connection_timeout(&self) -> Duration { Duration::from_secs(self.connection_timeout_secs.unwrap_or(2)) } pub fn validate(&self) -> Result<&str, String> { if let Some(size) = self.pool_size { if size == 0 { return Err("redis.pool_size must be at least 1".to_string()); } if size > 128 { return Err(format!("redis.pool_size must not exceed 128, got {size}")); } } let url = self .url .as_deref() .filter(|s| !s.is_empty()) .ok_or_else(|| "redis.url must be set".to_string())?; match self.mode() { RedisMode::Standalone | RedisMode::ValkeyStandalone => { if !(url.starts_with("redis://") || url.starts_with("rediss://")) { return Err(format!( "redis.url must start with redis:// or rediss://, got {url:?}" )); } } RedisMode::Cluster => { if !(url.starts_with("redis://") || url.starts_with("rediss://") || url.contains(",")) { return Err(format!( "redis cluster url must start with redis:// or rediss://, or list multiple hosts comma-separated, got {url:?}" )); } } RedisMode::Sentinel => { if !(url.starts_with("redis://") || url.starts_with("rediss://")) { return Err(format!( "redis sentinel url must start with redis:// or rediss://, got {url:?}" )); } } } Ok(url) } }