use camel_component_api::CamelError;
use camel_component_api::parse_uri;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
pub enum RedisCommand {
Set,
Get,
Getset,
Setnx,
Setex,
Mget,
Mset,
Incr,
Incrby,
Decr,
Decrby,
Append,
Strlen,
Exists,
Del,
Expire,
Expireat,
Pexpire,
Pexpireat,
Ttl,
Keys,
Rename,
Renamenx,
Type,
Persist,
Move,
Sort,
Lpush,
Rpush,
Lpushx,
Rpushx,
Lpop,
Rpop,
Blpop,
Brpop,
Llen,
Lrange,
Lindex,
Linsert,
Lset,
Lrem,
Ltrim,
Rpoplpush,
Hset,
Hget,
Hsetnx,
Hmset,
Hmget,
Hdel,
Hexists,
Hlen,
Hkeys,
Hvals,
Hgetall,
Hincrby,
Sadd,
Srem,
Smembers,
Scard,
Sismember,
Spop,
Smove,
Sinter,
Sunion,
Sdiff,
Sinterstore,
Sunionstore,
Sdiffstore,
Srandmember,
Zadd,
Zrem,
Zrange,
Zrevrange,
Zrank,
Zrevrank,
Zscore,
Zcard,
Zincrby,
Zcount,
Zrangebyscore,
Zrevrangebyscore,
Zremrangebyrank,
Zremrangebyscore,
Zunionstore,
Zinterstore,
Publish,
Subscribe,
Psubscribe,
Ping,
Echo,
}
impl FromStr for RedisCommand {
type Err = CamelError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"SET" => Ok(RedisCommand::Set),
"GET" => Ok(RedisCommand::Get),
"GETSET" => Ok(RedisCommand::Getset),
"SETNX" => Ok(RedisCommand::Setnx),
"SETEX" => Ok(RedisCommand::Setex),
"MGET" => Ok(RedisCommand::Mget),
"MSET" => Ok(RedisCommand::Mset),
"INCR" => Ok(RedisCommand::Incr),
"INCRBY" => Ok(RedisCommand::Incrby),
"DECR" => Ok(RedisCommand::Decr),
"DECRBY" => Ok(RedisCommand::Decrby),
"APPEND" => Ok(RedisCommand::Append),
"STRLEN" => Ok(RedisCommand::Strlen),
"EXISTS" => Ok(RedisCommand::Exists),
"DEL" => Ok(RedisCommand::Del),
"EXPIRE" => Ok(RedisCommand::Expire),
"EXPIREAT" => Ok(RedisCommand::Expireat),
"PEXPIRE" => Ok(RedisCommand::Pexpire),
"PEXPIREAT" => Ok(RedisCommand::Pexpireat),
"TTL" => Ok(RedisCommand::Ttl),
"KEYS" => Ok(RedisCommand::Keys),
"RENAME" => Ok(RedisCommand::Rename),
"RENAMENX" => Ok(RedisCommand::Renamenx),
"TYPE" => Ok(RedisCommand::Type),
"PERSIST" => Ok(RedisCommand::Persist),
"MOVE" => Ok(RedisCommand::Move),
"SORT" => Ok(RedisCommand::Sort),
"LPUSH" => Ok(RedisCommand::Lpush),
"RPUSH" => Ok(RedisCommand::Rpush),
"LPUSHX" => Ok(RedisCommand::Lpushx),
"RPUSHX" => Ok(RedisCommand::Rpushx),
"LPOP" => Ok(RedisCommand::Lpop),
"RPOP" => Ok(RedisCommand::Rpop),
"BLPOP" => Ok(RedisCommand::Blpop),
"BRPOP" => Ok(RedisCommand::Brpop),
"LLEN" => Ok(RedisCommand::Llen),
"LRANGE" => Ok(RedisCommand::Lrange),
"LINDEX" => Ok(RedisCommand::Lindex),
"LINSERT" => Ok(RedisCommand::Linsert),
"LSET" => Ok(RedisCommand::Lset),
"LREM" => Ok(RedisCommand::Lrem),
"LTRIM" => Ok(RedisCommand::Ltrim),
"RPOPLPUSH" => Ok(RedisCommand::Rpoplpush),
"HSET" => Ok(RedisCommand::Hset),
"HGET" => Ok(RedisCommand::Hget),
"HSETNX" => Ok(RedisCommand::Hsetnx),
"HMSET" => Ok(RedisCommand::Hmset),
"HMGET" => Ok(RedisCommand::Hmget),
"HDEL" => Ok(RedisCommand::Hdel),
"HEXISTS" => Ok(RedisCommand::Hexists),
"HLEN" => Ok(RedisCommand::Hlen),
"HKEYS" => Ok(RedisCommand::Hkeys),
"HVALS" => Ok(RedisCommand::Hvals),
"HGETALL" => Ok(RedisCommand::Hgetall),
"HINCRBY" => Ok(RedisCommand::Hincrby),
"SADD" => Ok(RedisCommand::Sadd),
"SREM" => Ok(RedisCommand::Srem),
"SMEMBERS" => Ok(RedisCommand::Smembers),
"SCARD" => Ok(RedisCommand::Scard),
"SISMEMBER" => Ok(RedisCommand::Sismember),
"SPOP" => Ok(RedisCommand::Spop),
"SMOVE" => Ok(RedisCommand::Smove),
"SINTER" => Ok(RedisCommand::Sinter),
"SUNION" => Ok(RedisCommand::Sunion),
"SDIFF" => Ok(RedisCommand::Sdiff),
"SINTERSTORE" => Ok(RedisCommand::Sinterstore),
"SUNIONSTORE" => Ok(RedisCommand::Sunionstore),
"SDIFFSTORE" => Ok(RedisCommand::Sdiffstore),
"SRANDMEMBER" => Ok(RedisCommand::Srandmember),
"ZADD" => Ok(RedisCommand::Zadd),
"ZREM" => Ok(RedisCommand::Zrem),
"ZRANGE" => Ok(RedisCommand::Zrange),
"ZREVRANGE" => Ok(RedisCommand::Zrevrange),
"ZRANK" => Ok(RedisCommand::Zrank),
"ZREVRANK" => Ok(RedisCommand::Zrevrank),
"ZSCORE" => Ok(RedisCommand::Zscore),
"ZCARD" => Ok(RedisCommand::Zcard),
"ZINCRBY" => Ok(RedisCommand::Zincrby),
"ZCOUNT" => Ok(RedisCommand::Zcount),
"ZRANGEBYSCORE" => Ok(RedisCommand::Zrangebyscore),
"ZREVRANGEBYSCORE" => Ok(RedisCommand::Zrevrangebyscore),
"ZREMRANGEBYRANK" => Ok(RedisCommand::Zremrangebyrank),
"ZREMRANGEBYSCORE" => Ok(RedisCommand::Zremrangebyscore),
"ZUNIONSTORE" => Ok(RedisCommand::Zunionstore),
"ZINTERSTORE" => Ok(RedisCommand::Zinterstore),
"PUBLISH" => Ok(RedisCommand::Publish),
"SUBSCRIBE" => Ok(RedisCommand::Subscribe),
"PSUBSCRIBE" => Ok(RedisCommand::Psubscribe),
"PING" => Ok(RedisCommand::Ping),
"ECHO" => Ok(RedisCommand::Echo),
_ => Err(CamelError::InvalidUri(format!(
"Unknown Redis command: {}",
s
))),
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(default)]
pub struct RedisConfig {
pub host: String,
pub port: u16,
}
impl Default for RedisConfig {
fn default() -> Self {
Self {
host: "localhost".to_string(),
port: 6379,
}
}
}
impl RedisConfig {
pub fn with_host(mut self, v: impl Into<String>) -> Self {
self.host = v.into();
self
}
pub fn with_port(mut self, v: u16) -> Self {
self.port = v;
self
}
}
#[derive(Debug, Clone)]
pub struct RedisEndpointConfig {
pub host: Option<String>,
pub port: Option<u16>,
pub command: RedisCommand,
pub channels: Vec<String>,
pub key: Option<String>,
pub timeout: u64,
pub password: Option<String>,
pub db: u8,
}
impl RedisEndpointConfig {
pub fn from_uri(uri: &str) -> Result<Self, CamelError> {
let parts = parse_uri(uri)?;
if parts.scheme != "redis" {
return Err(CamelError::InvalidUri(format!(
"expected scheme 'redis', got '{}'",
parts.scheme
)));
}
let (host, port) = if parts.path.starts_with("//") {
let path = &parts.path[2..]; if path.is_empty() {
(None, None)
} else {
let (host_part, port_part) = match path.split_once(':') {
Some((h, p)) => (h, Some(p)),
None => (path, None),
};
let host = Some(host_part.to_string());
let port = port_part.and_then(|p| p.parse().ok());
(host, port)
}
} else {
(None, None)
};
let command = parts
.params
.get("command")
.map(|s| RedisCommand::from_str(s))
.transpose()?
.unwrap_or(RedisCommand::Set);
let channels = parts
.params
.get("channels")
.map(|s| s.split(',').map(String::from).collect())
.unwrap_or_default();
let key = parts.params.get("key").cloned();
let timeout = parts
.params
.get("timeout")
.and_then(|s| s.parse().ok())
.unwrap_or(1);
let password = parts.params.get("password").cloned();
let db = parts
.params
.get("db")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
Ok(Self {
host,
port,
command,
channels,
key,
timeout,
password,
db,
})
}
pub fn apply_defaults(&mut self, defaults: &RedisConfig) {
if self.host.is_none() {
self.host = Some(defaults.host.clone());
}
if self.port.is_none() {
self.port = Some(defaults.port);
}
}
pub fn resolve_defaults(&mut self) {
let defaults = RedisConfig::default();
if self.host.is_none() {
self.host = Some(defaults.host);
}
if self.port.is_none() {
self.port = Some(defaults.port);
}
}
pub fn redis_url(&self) -> String {
let host = self.host.as_deref().unwrap_or("localhost");
let port = self.port.unwrap_or(6379);
if let Some(password) = &self.password {
format!("redis://:{}@{}:{}/{}", password, host, port, self.db)
} else {
format!("redis://{}:{}/{}", host, port, self.db)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_defaults() {
let c = RedisEndpointConfig::from_uri("redis://localhost:6379").unwrap();
assert_eq!(c.host, Some("localhost".to_string()));
assert_eq!(c.port, Some(6379));
assert_eq!(c.command, RedisCommand::Set);
assert!(c.channels.is_empty());
assert!(c.key.is_none());
assert_eq!(c.timeout, 1);
assert!(c.password.is_none());
assert_eq!(c.db, 0);
}
#[test]
fn test_config_no_host_port() {
let c = RedisEndpointConfig::from_uri("redis://?command=GET").unwrap();
assert_eq!(c.host, None);
assert_eq!(c.port, None);
assert_eq!(c.command, RedisCommand::Get);
}
#[test]
fn test_config_host_only() {
let c = RedisEndpointConfig::from_uri("redis://redis-server?command=GET").unwrap();
assert_eq!(c.host, Some("redis-server".to_string()));
assert_eq!(c.port, None);
}
#[test]
fn test_config_host_and_port() {
let c = RedisEndpointConfig::from_uri("redis://localhost:6380?command=GET").unwrap();
assert_eq!(c.host, Some("localhost".to_string()));
assert_eq!(c.port, Some(6380));
assert_eq!(c.command, RedisCommand::Get);
}
#[test]
fn test_config_wrong_scheme() {
assert!(RedisEndpointConfig::from_uri("http://localhost:6379").is_err());
}
#[test]
fn test_config_command() {
let c = RedisEndpointConfig::from_uri("redis://localhost:6379?command=GET").unwrap();
assert_eq!(c.command, RedisCommand::Get);
}
#[test]
fn test_config_subscribe() {
let c = RedisEndpointConfig::from_uri(
"redis://localhost:6379?command=SUBSCRIBE&channels=foo,bar",
)
.unwrap();
assert_eq!(c.command, RedisCommand::Subscribe);
assert_eq!(c.channels, vec!["foo".to_string(), "bar".to_string()]);
}
#[test]
fn test_config_blpop() {
let c = RedisEndpointConfig::from_uri(
"redis://localhost:6379?command=BLPOP&key=jobs&timeout=5",
)
.unwrap();
assert_eq!(c.command, RedisCommand::Blpop);
assert_eq!(c.key, Some("jobs".to_string()));
assert_eq!(c.timeout, 5);
}
#[test]
fn test_config_auth_db() {
let c =
RedisEndpointConfig::from_uri("redis://localhost:6379?password=secret&db=2").unwrap();
assert_eq!(c.password, Some("secret".to_string()));
assert_eq!(c.db, 2);
}
#[test]
fn test_redis_url() {
let mut c =
RedisEndpointConfig::from_uri("redis://localhost:6379?password=secret&db=2").unwrap();
c.resolve_defaults();
assert_eq!(c.redis_url(), "redis://:secret@localhost:6379/2");
}
#[test]
fn test_redis_url_no_auth() {
let mut c = RedisEndpointConfig::from_uri("redis://localhost:6379").unwrap();
c.resolve_defaults();
assert_eq!(c.redis_url(), "redis://localhost:6379/0");
}
#[test]
fn test_command_from_str() {
assert!(RedisCommand::from_str("SET").is_ok());
assert_eq!(RedisCommand::from_str("SET").unwrap(), RedisCommand::Set);
assert!(RedisCommand::from_str("get").is_ok());
assert_eq!(RedisCommand::from_str("get").unwrap(), RedisCommand::Get);
assert!(RedisCommand::from_str("UNKNOWN").is_err());
}
#[test]
fn test_redis_config_defaults() {
let cfg = RedisConfig::default();
assert_eq!(cfg.host, "localhost");
assert_eq!(cfg.port, 6379);
}
#[test]
fn test_redis_config_builder() {
let cfg = RedisConfig::default()
.with_host("redis-prod")
.with_port(6380);
assert_eq!(cfg.host, "redis-prod");
assert_eq!(cfg.port, 6380);
}
#[test]
fn test_apply_defaults_fills_none_fields() {
let mut config = RedisEndpointConfig::from_uri("redis://?command=GET").unwrap();
assert_eq!(config.host, None);
assert_eq!(config.port, None);
let defaults = RedisConfig::default()
.with_host("redis-server")
.with_port(6380);
config.apply_defaults(&defaults);
assert_eq!(config.host, Some("redis-server".to_string()));
assert_eq!(config.port, Some(6380));
}
#[test]
fn test_apply_defaults_preserves_values() {
let mut config = RedisEndpointConfig::from_uri("redis://custom:7000?command=GET").unwrap();
assert_eq!(config.host, Some("custom".to_string()));
assert_eq!(config.port, Some(7000));
let defaults = RedisConfig::default()
.with_host("should-not-override")
.with_port(9999);
config.apply_defaults(&defaults);
assert_eq!(config.host, Some("custom".to_string()));
assert_eq!(config.port, Some(7000));
}
#[test]
fn test_apply_defaults_partial_none() {
let mut config = RedisEndpointConfig::from_uri("redis://myhost?command=GET").unwrap();
assert_eq!(config.host, Some("myhost".to_string()));
assert_eq!(config.port, None);
let defaults = RedisConfig::default()
.with_host("default-host")
.with_port(6380);
config.apply_defaults(&defaults);
assert_eq!(config.host, Some("myhost".to_string()));
assert_eq!(config.port, Some(6380));
}
#[test]
fn test_resolve_defaults_fills_remaining_nones() {
let mut config = RedisEndpointConfig::from_uri("redis://?command=GET").unwrap();
assert_eq!(config.host, None);
assert_eq!(config.port, None);
config.resolve_defaults();
assert_eq!(config.host, Some("localhost".to_string()));
assert_eq!(config.port, Some(6379));
}
#[test]
fn test_resolve_defaults_preserves_existing() {
let mut config = RedisEndpointConfig::from_uri("redis://custom:7000?command=GET").unwrap();
config.resolve_defaults();
assert_eq!(config.host, Some("custom".to_string()));
assert_eq!(config.port, Some(7000));
}
#[test]
fn test_apply_defaults_with_from_uri_no_host_param() {
let mut ep = RedisEndpointConfig::from_uri("redis://?command=GET").unwrap();
assert!(ep.host.is_none(), "host should be None when not in URI");
assert!(ep.port.is_none(), "port should be None when not in URI");
let defaults = RedisConfig::default().with_host("redis-prod");
ep.apply_defaults(&defaults);
assert_eq!(
ep.host,
Some("redis-prod".to_string()),
"custom default host should be applied"
);
}
}