pub mod bundle;
pub mod commands;
pub mod config;
pub mod consumer;
pub mod executor;
pub mod producer;
use camel_component_api::{BoxProcessor, CamelError};
use camel_component_api::{Component, Consumer, Endpoint, ProducerContext};
pub use bundle::RedisBundle;
pub use config::{RedisCommand, RedisConfig, RedisEndpointConfig};
pub use consumer::RedisConsumer;
pub use producer::RedisProducer;
pub struct RedisComponent {
config: Option<RedisConfig>,
}
impl RedisComponent {
pub fn new() -> Self {
Self { config: None }
}
pub fn with_config(config: RedisConfig) -> Self {
Self {
config: Some(config),
}
}
pub fn with_optional_config(config: Option<RedisConfig>) -> Self {
Self { config }
}
}
impl Default for RedisComponent {
fn default() -> Self {
Self::new()
}
}
impl Component for RedisComponent {
fn scheme(&self) -> &str {
"redis"
}
fn create_endpoint(
&self,
uri: &str,
_ctx: &dyn camel_component_api::ComponentContext,
) -> Result<Box<dyn Endpoint>, CamelError> {
let mut config = RedisEndpointConfig::from_uri(uri)?;
if let Some(ref global_cfg) = self.config {
config.apply_defaults(global_cfg);
}
config.resolve_defaults();
Ok(Box::new(RedisEndpoint {
uri: uri.to_string(),
config,
}))
}
}
pub struct RedisEndpoint {
uri: String,
config: RedisEndpointConfig,
}
impl Endpoint for RedisEndpoint {
fn uri(&self) -> &str {
&self.uri
}
fn create_producer(&self, _ctx: &ProducerContext) -> Result<BoxProcessor, CamelError> {
Ok(BoxProcessor::new(RedisProducer::new(self.config.clone())))
}
fn create_consumer(&self) -> Result<Box<dyn Consumer>, CamelError> {
Ok(Box::new(RedisConsumer::new(self.config.clone())?))
}
}
#[cfg(test)]
mod tests {
use super::*;
use camel_component_api::NoOpComponentContext;
#[test]
fn test_component_scheme() {
let component = RedisComponent::new();
assert_eq!(component.scheme(), "redis");
}
#[test]
fn test_component_creates_endpoint() {
let component = RedisComponent::new();
let ctx = NoOpComponentContext;
let endpoint = component
.create_endpoint("redis://localhost:6379?command=GET", &ctx)
.expect("endpoint should be created");
assert_eq!(endpoint.uri(), "redis://localhost:6379?command=GET");
}
#[test]
fn test_component_rejects_wrong_scheme() {
let component = RedisComponent::new();
let ctx = NoOpComponentContext;
let result = component.create_endpoint("kafka:topic?brokers=localhost:9092", &ctx);
assert!(result.is_err(), "wrong scheme should fail");
let err = result.err().expect("error must exist");
assert!(err.to_string().contains("expected scheme 'redis'"));
}
#[test]
fn test_component_applies_global_defaults() {
let global = RedisConfig::default()
.with_host("redis-global")
.with_port(6380);
let component = RedisComponent::with_config(global);
let ctx = NoOpComponentContext;
let endpoint = component
.create_endpoint("redis://?command=GET", &ctx)
.expect("endpoint should be created with defaults");
let _producer = endpoint
.create_producer(&ProducerContext::default())
.expect("producer should be created");
let endpoint2 = component
.create_endpoint("redis://?command=BLPOP&key=test", &ctx)
.expect("endpoint should be created");
let _consumer = endpoint2
.create_consumer()
.expect("consumer should be created");
}
#[test]
fn test_redis_endpoint_is_pub_accessible() {
let component = RedisComponent::new();
let ctx = NoOpComponentContext;
let endpoint = component
.create_endpoint("redis://localhost:6379?command=GET", &ctx)
.unwrap();
assert_eq!(endpoint.uri(), "redis://localhost:6379?command=GET");
}
}
#[cfg(test)]
mod integration_tests {
use crate::{RedisComponent, RedisEndpointConfig, RedisProducer};
use camel_component_api::NoOpComponentContext;
use camel_component_api::{Component, ProducerContext};
use std::time::Duration;
async fn redis_available() -> bool {
tokio::net::TcpStream::connect("127.0.0.1:6379")
.await
.is_ok()
}
#[tokio::test]
#[ignore = "Requires live Redis at 127.0.0.1:6379"]
async fn test_integration_ping() {
if !redis_available().await {
eprintln!("Skipping: Redis not available at 127.0.0.1:6379");
return;
}
let config = RedisEndpointConfig::from_uri("redis://127.0.0.1:6379?command=PING").unwrap();
let producer = RedisProducer::new(config);
producer
.check_connection()
.await
.expect("PING should succeed");
}
#[tokio::test]
#[ignore = "Requires live Redis at 127.0.0.1:6379"]
async fn test_integration_set_get() {
if !redis_available().await {
eprintln!("Skipping: Redis not available at 127.0.0.1:6379");
return;
}
let component = RedisComponent::new();
let ctx = NoOpComponentContext;
let endpoint = component
.create_endpoint("redis://127.0.0.1:6379?command=SET", &ctx)
.unwrap();
let _producer = endpoint
.create_producer(&ProducerContext::default())
.expect("producer should be created");
}
#[tokio::test]
#[ignore = "Requires live Redis at 127.0.0.1:6379"]
async fn test_integration_pubsub() {
if !redis_available().await {
eprintln!("Skipping: Redis not available at 127.0.0.1:6379");
return;
}
let component = RedisComponent::new();
let ctx = NoOpComponentContext;
let endpoint = component
.create_endpoint(
"redis://127.0.0.1:6379?command=SUBSCRIBE&channels=integration-test",
&ctx,
)
.unwrap();
let mut consumer = endpoint
.create_consumer()
.expect("consumer should be created");
let (tx, _rx) = tokio::sync::mpsc::channel(16);
let cancel_token = tokio_util::sync::CancellationToken::new();
let consumer_ctx = camel_component_api::ConsumerContext::new(tx, cancel_token.clone());
consumer
.start(consumer_ctx)
.await
.expect("start should succeed");
tokio::time::sleep(Duration::from_millis(200)).await;
cancel_token.cancel();
consumer.stop().await.expect("stop should succeed");
}
}