1use camel_api::CamelError;
2use camel_endpoint::parse_uri;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum RedisCommand {
7 Set,
9 Get,
10 Getset,
11 Setnx,
12 Setex,
13 Mget,
14 Mset,
15 Incr,
16 Incrby,
17 Decr,
18 Decrby,
19 Append,
20 Strlen,
21
22 Exists,
24 Del,
25 Expire,
26 Expireat,
27 Pexpire,
28 Pexpireat,
29 Ttl,
30 Keys,
31 Rename,
32 Renamenx,
33 Type,
34 Persist,
35 Move,
36 Sort,
37
38 Lpush,
40 Rpush,
41 Lpushx,
42 Rpushx,
43 Lpop,
44 Rpop,
45 Blpop,
46 Brpop,
47 Llen,
48 Lrange,
49 Lindex,
50 Linsert,
51 Lset,
52 Lrem,
53 Ltrim,
54 Rpoplpush,
55
56 Hset,
58 Hget,
59 Hsetnx,
60 Hmset,
61 Hmget,
62 Hdel,
63 Hexists,
64 Hlen,
65 Hkeys,
66 Hvals,
67 Hgetall,
68 Hincrby,
69
70 Sadd,
72 Srem,
73 Smembers,
74 Scard,
75 Sismember,
76 Spop,
77 Smove,
78 Sinter,
79 Sunion,
80 Sdiff,
81 Sinterstore,
82 Sunionstore,
83 Sdiffstore,
84 Srandmember,
85
86 Zadd,
88 Zrem,
89 Zrange,
90 Zrevrange,
91 Zrank,
92 Zrevrank,
93 Zscore,
94 Zcard,
95 Zincrby,
96 Zcount,
97 Zrangebyscore,
98 Zrevrangebyscore,
99 Zremrangebyrank,
100 Zremrangebyscore,
101 Zunionstore,
102 Zinterstore,
103
104 Publish,
106 Subscribe,
107 Psubscribe,
108
109 Ping,
111 Echo,
112}
113
114impl FromStr for RedisCommand {
115 type Err = CamelError;
116
117 fn from_str(s: &str) -> Result<Self, Self::Err> {
118 match s.to_uppercase().as_str() {
119 "SET" => Ok(RedisCommand::Set),
121 "GET" => Ok(RedisCommand::Get),
122 "GETSET" => Ok(RedisCommand::Getset),
123 "SETNX" => Ok(RedisCommand::Setnx),
124 "SETEX" => Ok(RedisCommand::Setex),
125 "MGET" => Ok(RedisCommand::Mget),
126 "MSET" => Ok(RedisCommand::Mset),
127 "INCR" => Ok(RedisCommand::Incr),
128 "INCRBY" => Ok(RedisCommand::Incrby),
129 "DECR" => Ok(RedisCommand::Decr),
130 "DECRBY" => Ok(RedisCommand::Decrby),
131 "APPEND" => Ok(RedisCommand::Append),
132 "STRLEN" => Ok(RedisCommand::Strlen),
133
134 "EXISTS" => Ok(RedisCommand::Exists),
136 "DEL" => Ok(RedisCommand::Del),
137 "EXPIRE" => Ok(RedisCommand::Expire),
138 "EXPIREAT" => Ok(RedisCommand::Expireat),
139 "PEXPIRE" => Ok(RedisCommand::Pexpire),
140 "PEXPIREAT" => Ok(RedisCommand::Pexpireat),
141 "TTL" => Ok(RedisCommand::Ttl),
142 "KEYS" => Ok(RedisCommand::Keys),
143 "RENAME" => Ok(RedisCommand::Rename),
144 "RENAMENX" => Ok(RedisCommand::Renamenx),
145 "TYPE" => Ok(RedisCommand::Type),
146 "PERSIST" => Ok(RedisCommand::Persist),
147 "MOVE" => Ok(RedisCommand::Move),
148 "SORT" => Ok(RedisCommand::Sort),
149
150 "LPUSH" => Ok(RedisCommand::Lpush),
152 "RPUSH" => Ok(RedisCommand::Rpush),
153 "LPUSHX" => Ok(RedisCommand::Lpushx),
154 "RPUSHX" => Ok(RedisCommand::Rpushx),
155 "LPOP" => Ok(RedisCommand::Lpop),
156 "RPOP" => Ok(RedisCommand::Rpop),
157 "BLPOP" => Ok(RedisCommand::Blpop),
158 "BRPOP" => Ok(RedisCommand::Brpop),
159 "LLEN" => Ok(RedisCommand::Llen),
160 "LRANGE" => Ok(RedisCommand::Lrange),
161 "LINDEX" => Ok(RedisCommand::Lindex),
162 "LINSERT" => Ok(RedisCommand::Linsert),
163 "LSET" => Ok(RedisCommand::Lset),
164 "LREM" => Ok(RedisCommand::Lrem),
165 "LTRIM" => Ok(RedisCommand::Ltrim),
166 "RPOPLPUSH" => Ok(RedisCommand::Rpoplpush),
167
168 "HSET" => Ok(RedisCommand::Hset),
170 "HGET" => Ok(RedisCommand::Hget),
171 "HSETNX" => Ok(RedisCommand::Hsetnx),
172 "HMSET" => Ok(RedisCommand::Hmset),
173 "HMGET" => Ok(RedisCommand::Hmget),
174 "HDEL" => Ok(RedisCommand::Hdel),
175 "HEXISTS" => Ok(RedisCommand::Hexists),
176 "HLEN" => Ok(RedisCommand::Hlen),
177 "HKEYS" => Ok(RedisCommand::Hkeys),
178 "HVALS" => Ok(RedisCommand::Hvals),
179 "HGETALL" => Ok(RedisCommand::Hgetall),
180 "HINCRBY" => Ok(RedisCommand::Hincrby),
181
182 "SADD" => Ok(RedisCommand::Sadd),
184 "SREM" => Ok(RedisCommand::Srem),
185 "SMEMBERS" => Ok(RedisCommand::Smembers),
186 "SCARD" => Ok(RedisCommand::Scard),
187 "SISMEMBER" => Ok(RedisCommand::Sismember),
188 "SPOP" => Ok(RedisCommand::Spop),
189 "SMOVE" => Ok(RedisCommand::Smove),
190 "SINTER" => Ok(RedisCommand::Sinter),
191 "SUNION" => Ok(RedisCommand::Sunion),
192 "SDIFF" => Ok(RedisCommand::Sdiff),
193 "SINTERSTORE" => Ok(RedisCommand::Sinterstore),
194 "SUNIONSTORE" => Ok(RedisCommand::Sunionstore),
195 "SDIFFSTORE" => Ok(RedisCommand::Sdiffstore),
196 "SRANDMEMBER" => Ok(RedisCommand::Srandmember),
197
198 "ZADD" => Ok(RedisCommand::Zadd),
200 "ZREM" => Ok(RedisCommand::Zrem),
201 "ZRANGE" => Ok(RedisCommand::Zrange),
202 "ZREVRANGE" => Ok(RedisCommand::Zrevrange),
203 "ZRANK" => Ok(RedisCommand::Zrank),
204 "ZREVRANK" => Ok(RedisCommand::Zrevrank),
205 "ZSCORE" => Ok(RedisCommand::Zscore),
206 "ZCARD" => Ok(RedisCommand::Zcard),
207 "ZINCRBY" => Ok(RedisCommand::Zincrby),
208 "ZCOUNT" => Ok(RedisCommand::Zcount),
209 "ZRANGEBYSCORE" => Ok(RedisCommand::Zrangebyscore),
210 "ZREVRANGEBYSCORE" => Ok(RedisCommand::Zrevrangebyscore),
211 "ZREMRANGEBYRANK" => Ok(RedisCommand::Zremrangebyrank),
212 "ZREMRANGEBYSCORE" => Ok(RedisCommand::Zremrangebyscore),
213 "ZUNIONSTORE" => Ok(RedisCommand::Zunionstore),
214 "ZINTERSTORE" => Ok(RedisCommand::Zinterstore),
215
216 "PUBLISH" => Ok(RedisCommand::Publish),
218 "SUBSCRIBE" => Ok(RedisCommand::Subscribe),
219 "PSUBSCRIBE" => Ok(RedisCommand::Psubscribe),
220
221 "PING" => Ok(RedisCommand::Ping),
223 "ECHO" => Ok(RedisCommand::Echo),
224
225 _ => Err(CamelError::InvalidUri(format!(
226 "Unknown Redis command: {}",
227 s
228 ))),
229 }
230 }
231}
232
233#[derive(Debug, Clone)]
234pub struct RedisConfig {
235 pub host: String,
236 pub port: u16,
237 pub command: RedisCommand,
238 pub channels: Vec<String>,
239 pub key: Option<String>,
240 pub timeout: u64,
241 pub password: Option<String>,
242 pub db: u8,
243}
244
245impl RedisConfig {
246 pub fn from_uri(uri: &str) -> Result<Self, CamelError> {
247 let parts = parse_uri(uri)?;
248
249 if parts.scheme != "redis" {
250 return Err(CamelError::InvalidUri(format!(
251 "expected scheme 'redis', got '{}'",
252 parts.scheme
253 )));
254 }
255
256 let (host, port) = if parts.path.starts_with("//") {
258 let path = &parts.path[2..]; let (host_part, port_part) = match path.split_once(':') {
260 Some((h, p)) => (h, p),
261 None => (path, "6379"),
262 };
263 let port = port_part.parse().unwrap_or(6379);
264 (host_part.to_string(), port)
265 } else {
266 ("localhost".to_string(), 6379)
267 };
268
269 let command = parts
271 .params
272 .get("command")
273 .map(|s| RedisCommand::from_str(s))
274 .transpose()?
275 .unwrap_or(RedisCommand::Set);
276
277 let channels = parts
279 .params
280 .get("channels")
281 .map(|s| s.split(',').map(String::from).collect())
282 .unwrap_or_default();
283
284 let key = parts.params.get("key").cloned();
286
287 let timeout = parts
289 .params
290 .get("timeout")
291 .and_then(|s| s.parse().ok())
292 .unwrap_or(1);
293
294 let password = parts.params.get("password").cloned();
296
297 let db = parts
299 .params
300 .get("db")
301 .and_then(|s| s.parse().ok())
302 .unwrap_or(0);
303
304 Ok(Self {
305 host,
306 port,
307 command,
308 channels,
309 key,
310 timeout,
311 password,
312 db,
313 })
314 }
315
316 pub fn redis_url(&self) -> String {
317 if let Some(password) = &self.password {
318 format!(
319 "redis://:{}@{}:{}/{}",
320 password, self.host, self.port, self.db
321 )
322 } else {
323 format!("redis://{}:{}/{}", self.host, self.port, self.db)
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn test_config_defaults() {
334 let c = RedisConfig::from_uri("redis://localhost:6379").unwrap();
335 assert_eq!(c.host, "localhost");
336 assert_eq!(c.port, 6379);
337 assert_eq!(c.command, RedisCommand::Set);
338 assert!(c.channels.is_empty());
339 assert!(c.key.is_none());
340 assert_eq!(c.timeout, 1);
341 assert!(c.password.is_none());
342 assert_eq!(c.db, 0);
343 }
344
345 #[test]
346 fn test_config_wrong_scheme() {
347 assert!(RedisConfig::from_uri("http://localhost:6379").is_err());
348 }
349
350 #[test]
351 fn test_config_command() {
352 let c = RedisConfig::from_uri("redis://localhost:6379?command=GET").unwrap();
353 assert_eq!(c.command, RedisCommand::Get);
354 }
355
356 #[test]
357 fn test_config_subscribe() {
358 let c = RedisConfig::from_uri("redis://localhost:6379?command=SUBSCRIBE&channels=foo,bar")
359 .unwrap();
360 assert_eq!(c.command, RedisCommand::Subscribe);
361 assert_eq!(c.channels, vec!["foo".to_string(), "bar".to_string()]);
362 }
363
364 #[test]
365 fn test_config_blpop() {
366 let c = RedisConfig::from_uri("redis://localhost:6379?command=BLPOP&key=jobs&timeout=5")
367 .unwrap();
368 assert_eq!(c.command, RedisCommand::Blpop);
369 assert_eq!(c.key, Some("jobs".to_string()));
370 assert_eq!(c.timeout, 5);
371 }
372
373 #[test]
374 fn test_config_auth_db() {
375 let c = RedisConfig::from_uri("redis://localhost:6379?password=secret&db=2").unwrap();
376 assert_eq!(c.password, Some("secret".to_string()));
377 assert_eq!(c.db, 2);
378 }
379
380 #[test]
381 fn test_redis_url() {
382 let c = RedisConfig::from_uri("redis://localhost:6379?password=secret&db=2").unwrap();
383 assert_eq!(c.redis_url(), "redis://:secret@localhost:6379/2");
384 }
385
386 #[test]
387 fn test_redis_url_no_auth() {
388 let c = RedisConfig::from_uri("redis://localhost:6379").unwrap();
389 assert_eq!(c.redis_url(), "redis://localhost:6379/0");
390 }
391
392 #[test]
393 fn test_command_from_str() {
394 assert!(RedisCommand::from_str("SET").is_ok());
395 assert_eq!(RedisCommand::from_str("SET").unwrap(), RedisCommand::Set);
396 assert!(RedisCommand::from_str("get").is_ok());
397 assert_eq!(RedisCommand::from_str("get").unwrap(), RedisCommand::Get);
398 assert!(RedisCommand::from_str("UNKNOWN").is_err());
399 }
400}