camel_component_redis/
lib.rs1pub mod bundle;
29pub mod commands;
30pub mod config;
31pub mod consumer;
32pub mod executor;
33pub mod health;
34pub mod producer;
35
36use camel_component_api::{BoxProcessor, CamelError};
37use camel_component_api::{Component, Consumer, Endpoint, ProducerContext, RuntimeObservability};
38use std::sync::Arc;
39
40pub use bundle::RedisBundle;
41pub use config::{RedisCommand, RedisConfig, RedisEndpointConfig};
42pub use consumer::RedisConsumer;
43pub use health::RedisHealthCheck;
44pub use producer::RedisProducer;
45
46pub struct RedisComponent {
47 config: Option<RedisConfig>,
48}
49
50impl RedisComponent {
51 pub fn new() -> Self {
54 Self { config: None }
55 }
56
57 pub fn with_config(config: RedisConfig) -> Self {
60 Self {
61 config: Some(config),
62 }
63 }
64
65 pub fn with_optional_config(config: Option<RedisConfig>) -> Self {
68 Self { config }
69 }
70}
71
72impl Default for RedisComponent {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl Component for RedisComponent {
79 fn scheme(&self) -> &str {
80 "redis"
81 }
82
83 fn create_endpoint(
84 &self,
85 uri: &str,
86 ctx: &dyn camel_component_api::ComponentContext,
87 ) -> Result<Box<dyn Endpoint>, CamelError> {
88 let mut config = RedisEndpointConfig::from_uri(uri)?;
89 if let Some(ref global_cfg) = self.config {
91 config.apply_defaults(global_cfg);
92 }
93 config.resolve_defaults();
95
96 let health_check = RedisHealthCheck::new(&config)?;
97 ctx.register_current_route_health_check(Arc::new(health_check));
98
99 Ok(Box::new(RedisEndpoint {
100 uri: uri.to_string(),
101 config,
102 }))
103 }
104}
105
106pub struct RedisEndpoint {
107 uri: String,
108 config: RedisEndpointConfig,
109}
110
111impl Endpoint for RedisEndpoint {
112 fn uri(&self) -> &str {
113 &self.uri
114 }
115
116 fn create_producer(
117 &self,
118 _rt: Arc<dyn RuntimeObservability>,
119 _ctx: &ProducerContext,
120 ) -> Result<BoxProcessor, CamelError> {
121 Ok(BoxProcessor::new(RedisProducer::new(self.config.clone())))
122 }
123
124 fn create_consumer(
125 &self,
126 rt: Arc<dyn RuntimeObservability>,
127 ) -> Result<Box<dyn Consumer>, CamelError> {
128 Ok(Box::new(RedisConsumer::new(self.config.clone(), rt)?))
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use camel_component_api::test_support::PanicRuntimeObservability;
135 fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
136 std::sync::Arc::new(PanicRuntimeObservability)
137 }
138
139 use super::*;
140 use camel_component_api::NoOpComponentContext;
141
142 #[test]
143 fn test_component_scheme() {
144 let component = RedisComponent::new();
145 assert_eq!(component.scheme(), "redis");
146 }
147
148 #[test]
149 fn test_component_creates_endpoint() {
150 let component = RedisComponent::new();
151 let ctx = NoOpComponentContext;
152 let endpoint = component
153 .create_endpoint("redis://localhost:6379?command=GET", &ctx)
154 .expect("endpoint should be created");
155 assert_eq!(endpoint.uri(), "redis://localhost:6379?command=GET");
156 }
157
158 #[test]
159 fn test_component_rejects_wrong_scheme() {
160 let component = RedisComponent::new();
161 let ctx = NoOpComponentContext;
162 let result = component.create_endpoint("kafka:topic?brokers=localhost:9092", &ctx);
163 assert!(result.is_err(), "wrong scheme should fail");
164 let err = result.err().expect("error must exist");
165 assert!(err.to_string().contains("expected scheme 'redis'"));
166 }
167
168 #[test]
169 fn test_component_applies_global_defaults() {
170 let global = RedisConfig::default()
171 .with_host("localhost")
172 .with_port(6380);
173 let component = RedisComponent::with_config(global);
174 let ctx = NoOpComponentContext;
175
176 let endpoint = component
177 .create_endpoint("redis://?command=GET", &ctx)
178 .expect("endpoint should be created with defaults");
179
180 let _producer = endpoint
181 .create_producer(rt(), &ProducerContext::default())
182 .expect("producer should be created");
183 let endpoint2 = component
185 .create_endpoint("redis://?command=BLPOP&key=test", &ctx)
186 .expect("endpoint should be created");
187 let _consumer = endpoint2
188 .create_consumer(rt())
189 .expect("consumer should be created");
190 }
191
192 #[test]
194 fn test_redis_endpoint_is_pub_accessible() {
195 let component = RedisComponent::new();
196 let ctx = NoOpComponentContext;
197 let endpoint = component
198 .create_endpoint("redis://localhost:6379?command=GET", &ctx)
199 .unwrap();
200 assert_eq!(endpoint.uri(), "redis://localhost:6379?command=GET");
202 }
203}
204
205#[cfg(test)]
207mod integration_tests {
208 use crate::{RedisComponent, RedisEndpointConfig, RedisProducer};
209 use camel_component_api::NoOpComponentContext;
210 use camel_component_api::test_support::PanicRuntimeObservability;
211 use camel_component_api::{Component, ProducerContext};
212 fn test_rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
213 std::sync::Arc::new(PanicRuntimeObservability)
214 }
215 use std::time::Duration;
216
217 async fn redis_available() -> bool {
219 tokio::net::TcpStream::connect("127.0.0.1:6379")
220 .await
221 .is_ok()
222 }
223
224 #[tokio::test]
227 #[ignore = "Requires live Redis at 127.0.0.1:6379"]
228 async fn test_integration_ping() {
229 if !redis_available().await {
230 eprintln!("Skipping: Redis not available at 127.0.0.1:6379");
231 return;
232 }
233 let config = RedisEndpointConfig::from_uri("redis://127.0.0.1:6379?command=PING").unwrap();
234 let producer = RedisProducer::new(config);
235 producer
236 .check_connection()
237 .await
238 .expect("PING should succeed");
239 }
240
241 #[tokio::test]
244 #[ignore = "Requires live Redis at 127.0.0.1:6379"]
245 async fn test_integration_set_get() {
246 if !redis_available().await {
247 eprintln!("Skipping: Redis not available at 127.0.0.1:6379");
248 return;
249 }
250 let component = RedisComponent::new();
251 let ctx = NoOpComponentContext;
252 let endpoint = component
253 .create_endpoint("redis://127.0.0.1:6379?command=SET", &ctx)
254 .unwrap();
255
256 let _producer = endpoint
257 .create_producer(test_rt(), &ProducerContext::default())
258 .expect("producer should be created");
259 }
263
264 #[tokio::test]
267 #[ignore = "Requires live Redis at 127.0.0.1:6379"]
268 async fn test_integration_pubsub() {
269 if !redis_available().await {
270 eprintln!("Skipping: Redis not available at 127.0.0.1:6379");
271 return;
272 }
273 let component = RedisComponent::new();
274 let ctx = NoOpComponentContext;
275 let endpoint = component
276 .create_endpoint(
277 "redis://127.0.0.1:6379?command=SUBSCRIBE&channels=integration-test",
278 &ctx,
279 )
280 .unwrap();
281
282 let mut consumer = endpoint
283 .create_consumer(test_rt())
284 .expect("consumer should be created");
285
286 let (tx, _rx) = tokio::sync::mpsc::channel(16);
287 let cancel_token = tokio_util::sync::CancellationToken::new();
288 let consumer_ctx = camel_component_api::ConsumerContext::new(
289 tx,
290 cancel_token.clone(),
291 "redis-test-route".to_string(),
292 );
293
294 consumer
296 .start(consumer_ctx)
297 .await
298 .expect("start should succeed");
299 tokio::time::sleep(Duration::from_millis(200)).await;
300 cancel_token.cancel();
301 consumer.stop().await.expect("stop should succeed");
302 }
303}