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, PartialEq)]
240pub struct RedisConfig {
241 pub host: String,
242 pub port: u16,
243}
244
245impl Default for RedisConfig {
246 fn default() -> Self {
247 Self {
248 host: "localhost".to_string(),
249 port: 6379,
250 }
251 }
252}
253
254impl RedisConfig {
255 pub fn with_host(mut self, v: impl Into<String>) -> Self {
256 self.host = v.into();
257 self
258 }
259
260 pub fn with_port(mut self, v: u16) -> Self {
261 self.port = v;
262 self
263 }
264}
265
266#[derive(Debug, Clone)]
292pub struct RedisEndpointConfig {
293 pub host: Option<String>,
296
297 pub port: Option<u16>,
300
301 pub command: RedisCommand,
303
304 pub channels: Vec<String>,
306
307 pub key: Option<String>,
309
310 pub timeout: u64,
312
313 pub password: Option<String>,
315
316 pub db: u8,
318}
319
320impl RedisEndpointConfig {
321 pub fn from_uri(uri: &str) -> Result<Self, CamelError> {
322 let parts = parse_uri(uri)?;
323
324 if parts.scheme != "redis" {
325 return Err(CamelError::InvalidUri(format!(
326 "expected scheme 'redis', got '{}'",
327 parts.scheme
328 )));
329 }
330
331 let (host, port) = if parts.path.starts_with("//") {
334 let path = &parts.path[2..]; if path.is_empty() {
336 (None, None)
338 } else {
339 let (host_part, port_part) = match path.split_once(':') {
340 Some((h, p)) => (h, Some(p)),
341 None => (path, None),
342 };
343 let host = Some(host_part.to_string());
344 let port = port_part.and_then(|p| p.parse().ok());
345 (host, port)
346 }
347 } else {
348 (None, None)
350 };
351
352 let command = parts
354 .params
355 .get("command")
356 .map(|s| RedisCommand::from_str(s))
357 .transpose()?
358 .unwrap_or(RedisCommand::Set);
359
360 let channels = parts
362 .params
363 .get("channels")
364 .map(|s| s.split(',').map(String::from).collect())
365 .unwrap_or_default();
366
367 let key = parts.params.get("key").cloned();
369
370 let timeout = parts
372 .params
373 .get("timeout")
374 .and_then(|s| s.parse().ok())
375 .unwrap_or(1);
376
377 let password = parts.params.get("password").cloned();
379
380 let db = parts
382 .params
383 .get("db")
384 .and_then(|s| s.parse().ok())
385 .unwrap_or(0);
386
387 Ok(Self {
388 host,
389 port,
390 command,
391 channels,
392 key,
393 timeout,
394 password,
395 db,
396 })
397 }
398
399 pub fn apply_defaults(&mut self, defaults: &RedisConfig) {
405 if self.host.is_none() {
406 self.host = Some(defaults.host.clone());
407 }
408 if self.port.is_none() {
409 self.port = Some(defaults.port);
410 }
411 }
412
413 pub fn resolve_defaults(&mut self) {
418 let defaults = RedisConfig::default();
419 if self.host.is_none() {
420 self.host = Some(defaults.host);
421 }
422 if self.port.is_none() {
423 self.port = Some(defaults.port);
424 }
425 }
426
427 pub fn redis_url(&self) -> String {
431 let host = self.host.as_deref().unwrap_or("localhost");
432 let port = self.port.unwrap_or(6379);
433
434 if let Some(password) = &self.password {
435 format!("redis://:{}@{}:{}/{}", password, host, port, self.db)
436 } else {
437 format!("redis://{}:{}/{}", host, port, self.db)
438 }
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn test_config_defaults() {
448 let c = RedisEndpointConfig::from_uri("redis://localhost:6379").unwrap();
449 assert_eq!(c.host, Some("localhost".to_string()));
450 assert_eq!(c.port, Some(6379));
451 assert_eq!(c.command, RedisCommand::Set);
452 assert!(c.channels.is_empty());
453 assert!(c.key.is_none());
454 assert_eq!(c.timeout, 1);
455 assert!(c.password.is_none());
456 assert_eq!(c.db, 0);
457 }
458
459 #[test]
460 fn test_config_no_host_port() {
461 let c = RedisEndpointConfig::from_uri("redis://?command=GET").unwrap();
463 assert_eq!(c.host, None);
464 assert_eq!(c.port, None);
465 assert_eq!(c.command, RedisCommand::Get);
466 }
467
468 #[test]
469 fn test_config_host_only() {
470 let c = RedisEndpointConfig::from_uri("redis://redis-server?command=GET").unwrap();
472 assert_eq!(c.host, Some("redis-server".to_string()));
473 assert_eq!(c.port, None);
474 }
475
476 #[test]
477 fn test_config_host_and_port() {
478 let c = RedisEndpointConfig::from_uri("redis://localhost:6380?command=GET").unwrap();
479 assert_eq!(c.host, Some("localhost".to_string()));
480 assert_eq!(c.port, Some(6380));
481 assert_eq!(c.command, RedisCommand::Get);
482 }
483
484 #[test]
485 fn test_config_wrong_scheme() {
486 assert!(RedisEndpointConfig::from_uri("http://localhost:6379").is_err());
487 }
488
489 #[test]
490 fn test_config_command() {
491 let c = RedisEndpointConfig::from_uri("redis://localhost:6379?command=GET").unwrap();
492 assert_eq!(c.command, RedisCommand::Get);
493 }
494
495 #[test]
496 fn test_config_subscribe() {
497 let c = RedisEndpointConfig::from_uri(
498 "redis://localhost:6379?command=SUBSCRIBE&channels=foo,bar",
499 )
500 .unwrap();
501 assert_eq!(c.command, RedisCommand::Subscribe);
502 assert_eq!(c.channels, vec!["foo".to_string(), "bar".to_string()]);
503 }
504
505 #[test]
506 fn test_config_blpop() {
507 let c = RedisEndpointConfig::from_uri(
508 "redis://localhost:6379?command=BLPOP&key=jobs&timeout=5",
509 )
510 .unwrap();
511 assert_eq!(c.command, RedisCommand::Blpop);
512 assert_eq!(c.key, Some("jobs".to_string()));
513 assert_eq!(c.timeout, 5);
514 }
515
516 #[test]
517 fn test_config_auth_db() {
518 let c =
519 RedisEndpointConfig::from_uri("redis://localhost:6379?password=secret&db=2").unwrap();
520 assert_eq!(c.password, Some("secret".to_string()));
521 assert_eq!(c.db, 2);
522 }
523
524 #[test]
525 fn test_redis_url() {
526 let mut c =
527 RedisEndpointConfig::from_uri("redis://localhost:6379?password=secret&db=2").unwrap();
528 c.resolve_defaults();
529 assert_eq!(c.redis_url(), "redis://:secret@localhost:6379/2");
530 }
531
532 #[test]
533 fn test_redis_url_no_auth() {
534 let mut c = RedisEndpointConfig::from_uri("redis://localhost:6379").unwrap();
535 c.resolve_defaults();
536 assert_eq!(c.redis_url(), "redis://localhost:6379/0");
537 }
538
539 #[test]
540 fn test_command_from_str() {
541 assert!(RedisCommand::from_str("SET").is_ok());
542 assert_eq!(RedisCommand::from_str("SET").unwrap(), RedisCommand::Set);
543 assert!(RedisCommand::from_str("get").is_ok());
544 assert_eq!(RedisCommand::from_str("get").unwrap(), RedisCommand::Get);
545 assert!(RedisCommand::from_str("UNKNOWN").is_err());
546 }
547
548 #[test]
551 fn test_redis_config_defaults() {
552 let cfg = RedisConfig::default();
553 assert_eq!(cfg.host, "localhost");
554 assert_eq!(cfg.port, 6379);
555 }
556
557 #[test]
558 fn test_redis_config_builder() {
559 let cfg = RedisConfig::default()
560 .with_host("redis-prod")
561 .with_port(6380);
562 assert_eq!(cfg.host, "redis-prod");
563 assert_eq!(cfg.port, 6380);
564 }
565
566 #[test]
569 fn test_apply_defaults_fills_none_fields() {
570 let mut config = RedisEndpointConfig::from_uri("redis://?command=GET").unwrap();
571 assert_eq!(config.host, None);
572 assert_eq!(config.port, None);
573
574 let defaults = RedisConfig::default()
575 .with_host("redis-server")
576 .with_port(6380);
577 config.apply_defaults(&defaults);
578
579 assert_eq!(config.host, Some("redis-server".to_string()));
580 assert_eq!(config.port, Some(6380));
581 }
582
583 #[test]
584 fn test_apply_defaults_preserves_values() {
585 let mut config = RedisEndpointConfig::from_uri("redis://custom:7000?command=GET").unwrap();
586 assert_eq!(config.host, Some("custom".to_string()));
587 assert_eq!(config.port, Some(7000));
588
589 let defaults = RedisConfig::default()
590 .with_host("should-not-override")
591 .with_port(9999);
592 config.apply_defaults(&defaults);
593
594 assert_eq!(config.host, Some("custom".to_string()));
596 assert_eq!(config.port, Some(7000));
597 }
598
599 #[test]
600 fn test_apply_defaults_partial_none() {
601 let mut config = RedisEndpointConfig::from_uri("redis://myhost?command=GET").unwrap();
603 assert_eq!(config.host, Some("myhost".to_string()));
604 assert_eq!(config.port, None);
605
606 let defaults = RedisConfig::default()
607 .with_host("default-host")
608 .with_port(6380);
609 config.apply_defaults(&defaults);
610
611 assert_eq!(config.host, Some("myhost".to_string()));
613 assert_eq!(config.port, Some(6380));
614 }
615
616 #[test]
619 fn test_resolve_defaults_fills_remaining_nones() {
620 let mut config = RedisEndpointConfig::from_uri("redis://?command=GET").unwrap();
621 assert_eq!(config.host, None);
622 assert_eq!(config.port, None);
623
624 config.resolve_defaults();
625
626 assert_eq!(config.host, Some("localhost".to_string()));
627 assert_eq!(config.port, Some(6379));
628 }
629
630 #[test]
631 fn test_resolve_defaults_preserves_existing() {
632 let mut config = RedisEndpointConfig::from_uri("redis://custom:7000?command=GET").unwrap();
633 config.resolve_defaults();
634
635 assert_eq!(config.host, Some("custom".to_string()));
637 assert_eq!(config.port, Some(7000));
638 }
639
640 #[test]
643 fn test_apply_defaults_with_from_uri_no_host_param() {
644 let mut ep = RedisEndpointConfig::from_uri("redis://?command=GET").unwrap();
646 assert!(ep.host.is_none(), "host should be None when not in URI");
648 assert!(ep.port.is_none(), "port should be None when not in URI");
649
650 let defaults = RedisConfig::default().with_host("redis-prod");
651 ep.apply_defaults(&defaults);
652 assert_eq!(
654 ep.host,
655 Some("redis-prod".to_string()),
656 "custom default host should be applied"
657 );
658 }
659}