Skip to main content

camel_component_redis/
config.rs

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    // String operations
8    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    // Key operations
23    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    // List operations
39    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    // Hash operations
57    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    // Set operations
71    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    // Sorted set operations
87    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    // Pub/Sub operations
105    Publish,
106    Subscribe,
107    Psubscribe,
108
109    // Other operations
110    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            // String operations
120            "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            // Key operations
135            "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            // List operations
151            "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            // Hash operations
169            "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            // Set operations
183            "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            // Sorted set operations
199            "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            // Pub/Sub operations
217            "PUBLISH" => Ok(RedisCommand::Publish),
218            "SUBSCRIBE" => Ok(RedisCommand::Subscribe),
219            "PSUBSCRIBE" => Ok(RedisCommand::Psubscribe),
220
221            // Other operations
222            "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// --- RedisConfig (global defaults) ---
234
235/// Global Redis configuration defaults.
236///
237/// This struct holds component-level defaults that can be set via YAML config
238/// and applied to endpoint configurations when specific values aren't provided.
239#[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// --- RedisEndpointConfig (parsed from URI) ---
268
269/// Configuration parsed from a Redis URI.
270///
271/// Format: `redis://host:port?command=GET&...` or `redis://?command=GET` (no host/port)
272///
273/// # Fields with Global Defaults (Option<T>)
274///
275/// These fields can be set via global defaults in `Camel.toml`. They are `Option<T>`
276/// to distinguish between "not set by URI" (`None`) and "explicitly set by URI" (`Some(v)`).
277/// After calling `apply_defaults()` + `resolve_defaults()`, all are guaranteed `Some`.
278///
279/// - `host` - Redis server hostname
280/// - `port` - Redis server port
281///
282/// # Fields Without Global Defaults
283///
284/// These fields are per-endpoint only and have no global defaults:
285///
286/// - `command` - Redis command to execute (default: SET)
287/// - `channels` - Channels for pub/sub operations (default: empty)
288/// - `key` - Key for operations that require it (default: None)
289/// - `timeout` - Timeout in seconds for blocking operations (default: 1)
290/// - `password` - Redis password for authentication (default: None)
291/// - `db` - Redis database number (default: 0)
292#[derive(Debug, Clone)]
293pub struct RedisEndpointConfig {
294    /// Redis server hostname. `None` if not set in URI.
295    /// Filled by `apply_defaults()` from global config, then `resolve_defaults()`.
296    pub host: Option<String>,
297
298    /// Redis server port. `None` if not set in URI.
299    /// Filled by `apply_defaults()` from global config, then `resolve_defaults()`.
300    pub port: Option<u16>,
301
302    /// Redis command to execute. Default: SET.
303    pub command: RedisCommand,
304
305    /// Channels for pub/sub operations. Default: empty.
306    pub channels: Vec<String>,
307
308    /// Key for operations that require it. Default: None.
309    pub key: Option<String>,
310
311    /// Timeout in seconds for blocking operations. Default: 1.
312    pub timeout: u64,
313
314    /// Redis password for authentication. Default: None.
315    pub password: Option<String>,
316
317    /// Redis database number. Default: 0.
318    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        // Parse host and port from path (format: //host:port or //host or empty)
333        // Use Option to distinguish "not set in URI" from "set in URI"
334        let (host, port) = if parts.path.starts_with("//") {
335            let path = &parts.path[2..]; // Remove leading //
336            if path.is_empty() {
337                // redis://?command=GET → no host, no port
338                (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            // No // prefix means no host/port in URI
350            (None, None)
351        };
352
353        // Parse command (default to SET)
354        let command = parts
355            .params
356            .get("command")
357            .map(|s| RedisCommand::from_str(s))
358            .transpose()?
359            .unwrap_or(RedisCommand::Set);
360
361        // Parse channels (comma-separated)
362        let channels = parts
363            .params
364            .get("channels")
365            .map(|s| s.split(',').map(String::from).collect())
366            .unwrap_or_default();
367
368        // Parse key
369        let key = parts.params.get("key").cloned();
370
371        // Parse timeout (default to 1 second)
372        let timeout = parts
373            .params
374            .get("timeout")
375            .and_then(|s| s.parse().ok())
376            .unwrap_or(1);
377
378        // Parse password
379        let password = parts.params.get("password").cloned();
380
381        // Parse db (default to 0)
382        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    /// Apply global defaults to any `None` fields.
401    ///
402    /// This method fills in default values from the provided `RedisConfig` for
403    /// fields that are `None` (not set in URI). It's intended to be called after
404    /// parsing a URI when global component defaults should be applied.
405    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    /// Resolve any remaining `None` fields to hardcoded defaults.
415    ///
416    /// This should be called after `apply_defaults()` to ensure all fields
417    /// that can have global defaults are guaranteed to be `Some`.
418    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    /// Build the Redis connection URL.
429    ///
430    /// After `resolve_defaults()`, host and port are guaranteed `Some`.
431    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        // URI with no host/port in path
463        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        // URI with host but no port
472        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    // --- RedisConfig tests ---
550
551    #[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    // --- apply_defaults tests ---
568
569    #[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        // Existing values should be preserved
596        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        // Host set, port not set
603        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        // Host preserved, port filled
613        assert_eq!(config.host, Some("myhost".to_string()));
614        assert_eq!(config.port, Some(6380));
615    }
616
617    // --- resolve_defaults tests ---
618
619    #[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        // Already set values should be preserved
637        assert_eq!(config.host, Some("custom".to_string()));
638        assert_eq!(config.port, Some(7000));
639    }
640
641    // --- Critical test: catches the original defect where from_uri filled absent
642    //     params with hardcoded defaults, preventing apply_defaults from working.
643    #[test]
644    fn test_apply_defaults_with_from_uri_no_host_param() {
645        // Parse URI without host/port
646        let mut ep = RedisEndpointConfig::from_uri("redis://?command=GET").unwrap();
647        // At this point, ep.host and ep.port should be None
648        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        // Custom default should be applied
654        assert_eq!(
655            ep.host,
656            Some("redis-prod".to_string()),
657            "custom default host should be applied"
658        );
659    }
660}