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