//! Message Queue Configuration use serde::{Deserialize, Serialize}; use std::time::Duration; use super::{ExchangeType, MqError, MqResult}; /// Message queue configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MessageQueueConfig { /// Whether message queue is enabled #[serde(default = "default_enabled")] pub enabled: bool, /// Message queue type (rabbitmq or redis) #[serde(default = "default_type")] pub r#type: String, /// RabbitMQ configuration #[serde(default)] pub rabbitmq: RabbitMqConfig, } impl Default for MessageQueueConfig { fn default() -> Self { Self { enabled: true, r#type: "rabbitmq".to_string(), rabbitmq: RabbitMqConfig::default(), } } } /// RabbitMQ configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RabbitMqConfig { /// RabbitMQ host #[serde(default = "default_host")] pub host: String, /// RabbitMQ port #[serde(default = "default_port")] pub port: u16, /// RabbitMQ username #[serde(default = "default_username")] pub username: String, /// RabbitMQ password #[serde(default = "default_password")] pub password: String, /// RabbitMQ virtual host #[serde(default = "default_vhost")] pub vhost: String, /// Connection pool size #[serde(default = "default_pool_size")] pub pool_size: usize, /// Connection timeout in seconds #[serde(default = "default_connection_timeout")] pub connection_timeout_secs: u64, /// Heartbeat interval in seconds #[serde(default = "default_heartbeat")] pub heartbeat_secs: u64, /// Reconnection delay in seconds #[serde(default = "default_reconnect_delay")] pub reconnect_delay_secs: u64, /// Maximum reconnection attempts (0 = infinite) #[serde(default = "default_max_reconnect_attempts")] pub max_reconnect_attempts: u32, /// Confirm publish (wait for broker confirmation) #[serde(default = "default_confirm_publish")] pub confirm_publish: bool, /// Publish timeout in seconds #[serde(default = "default_publish_timeout")] pub publish_timeout_secs: u64, /// Consumer prefetch count #[serde(default = "default_prefetch_count")] pub prefetch_count: u16, /// Consumer timeout in seconds #[serde(default = "default_consumer_timeout")] pub consumer_timeout_secs: u64, /// Queue configurations #[serde(default)] pub queues: QueuesConfig, /// Exchange configurations #[serde(default)] pub exchanges: ExchangesConfig, /// Dead letter queue configuration #[serde(default)] pub dead_letter: DeadLetterConfig, /// Worker queue message TTL in milliseconds (default 5 minutes) #[serde(default = "default_worker_queue_ttl")] pub worker_queue_ttl_ms: u64, } impl Default for RabbitMqConfig { fn default() -> Self { Self { host: default_host(), port: default_port(), username: default_username(), password: default_password(), vhost: default_vhost(), pool_size: default_pool_size(), connection_timeout_secs: default_connection_timeout(), heartbeat_secs: default_heartbeat(), reconnect_delay_secs: default_reconnect_delay(), max_reconnect_attempts: default_max_reconnect_attempts(), confirm_publish: default_confirm_publish(), publish_timeout_secs: default_publish_timeout(), prefetch_count: default_prefetch_count(), consumer_timeout_secs: default_consumer_timeout(), queues: QueuesConfig::default(), exchanges: ExchangesConfig::default(), dead_letter: DeadLetterConfig::default(), worker_queue_ttl_ms: default_worker_queue_ttl(), } } } impl RabbitMqConfig { /// Get connection URL pub fn connection_url(&self) -> String { format!( "amqp://{}:{}@{}:{}/{}", self.username, self.password, self.host, self.port, self.vhost ) } /// Get connection timeout as Duration pub fn connection_timeout(&self) -> Duration { Duration::from_secs(self.connection_timeout_secs) } /// Get heartbeat as Duration pub fn heartbeat(&self) -> Duration { Duration::from_secs(self.heartbeat_secs) } /// Get reconnect delay as Duration pub fn reconnect_delay(&self) -> Duration { Duration::from_secs(self.reconnect_delay_secs) } /// Get publish timeout as Duration pub fn publish_timeout(&self) -> Duration { Duration::from_secs(self.publish_timeout_secs) } /// Get consumer timeout as Duration pub fn consumer_timeout(&self) -> Duration { Duration::from_secs(self.consumer_timeout_secs) } /// Get worker queue TTL as Duration pub fn worker_queue_ttl(&self) -> Duration { Duration::from_millis(self.worker_queue_ttl_ms) } /// Validate configuration pub fn validate(&self) -> MqResult<()> { if self.host.is_empty() { return Err(MqError::Config("Host cannot be empty".to_string())); } if self.username.is_empty() { return Err(MqError::Config("Username cannot be empty".to_string())); } if self.pool_size == 0 { return Err(MqError::Config( "Pool size must be greater than 0".to_string(), )); } Ok(()) } } /// Queue configurations #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QueuesConfig { /// Events queue configuration pub events: QueueConfig, /// Executions queue configuration (legacy - to be deprecated) pub executions: QueueConfig, /// Enforcement created queue configuration pub enforcements: QueueConfig, /// Execution requests queue configuration pub execution_requests: QueueConfig, /// Execution status updates queue configuration pub execution_status: QueueConfig, /// Execution completed queue configuration pub execution_completed: QueueConfig, /// Inquiry responses queue configuration pub inquiry_responses: QueueConfig, /// Notifications queue configuration pub notifications: QueueConfig, } impl Default for QueuesConfig { fn default() -> Self { Self { events: QueueConfig { name: "attune.events.queue".to_string(), durable: true, exclusive: false, auto_delete: false, }, executions: QueueConfig { name: "attune.executions.queue".to_string(), durable: true, exclusive: false, auto_delete: false, }, enforcements: QueueConfig { name: "attune.enforcements.queue".to_string(), durable: true, exclusive: false, auto_delete: false, }, execution_requests: QueueConfig { name: "attune.execution.requests.queue".to_string(), durable: true, exclusive: false, auto_delete: false, }, execution_status: QueueConfig { name: "attune.execution.status.queue".to_string(), durable: true, exclusive: false, auto_delete: false, }, execution_completed: QueueConfig { name: "attune.execution.completed.queue".to_string(), durable: true, exclusive: false, auto_delete: false, }, inquiry_responses: QueueConfig { name: "attune.inquiry.responses.queue".to_string(), durable: true, exclusive: false, auto_delete: false, }, notifications: QueueConfig { name: "attune.notifications.queue".to_string(), durable: true, exclusive: false, auto_delete: false, }, } } } /// Queue configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QueueConfig { /// Queue name pub name: String, /// Durable (survives broker restart) #[serde(default = "default_true")] pub durable: bool, /// Exclusive (only accessible by this connection) #[serde(default)] pub exclusive: bool, /// Auto-delete (deleted when last consumer disconnects) #[serde(default)] pub auto_delete: bool, } /// Exchange configurations #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExchangesConfig { /// Events exchange configuration pub events: ExchangeConfig, /// Executions exchange configuration pub executions: ExchangeConfig, /// Notifications exchange configuration pub notifications: ExchangeConfig, } impl Default for ExchangesConfig { fn default() -> Self { Self { events: ExchangeConfig { name: "attune.events".to_string(), r#type: ExchangeType::Topic, durable: true, auto_delete: false, }, executions: ExchangeConfig { name: "attune.executions".to_string(), r#type: ExchangeType::Topic, durable: true, auto_delete: false, }, notifications: ExchangeConfig { name: "attune.notifications".to_string(), r#type: ExchangeType::Fanout, durable: true, auto_delete: false, }, } } } /// Exchange configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExchangeConfig { /// Exchange name pub name: String, /// Exchange type pub r#type: ExchangeType, /// Durable (survives broker restart) #[serde(default = "default_true")] pub durable: bool, /// Auto-delete (deleted when last queue unbinds) #[serde(default)] pub auto_delete: bool, } /// Dead letter queue configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeadLetterConfig { /// Enable dead letter queues #[serde(default = "default_enabled")] pub enabled: bool, /// Dead letter exchange name #[serde(default = "default_dlx_exchange")] pub exchange: String, /// Message TTL in dead letter queue (milliseconds) #[serde(default = "default_dlq_ttl")] pub ttl_ms: u64, } impl Default for DeadLetterConfig { fn default() -> Self { Self { enabled: true, exchange: "attune.dlx".to_string(), ttl_ms: 86400000, // 24 hours } } } impl DeadLetterConfig { /// Get TTL as Duration pub fn ttl(&self) -> Duration { Duration::from_millis(self.ttl_ms) } } /// Publisher configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PublisherConfig { /// Confirm publish (wait for broker confirmation) #[serde(default = "default_confirm_publish")] pub confirm_publish: bool, /// Publish timeout in seconds #[serde(default = "default_publish_timeout")] pub timeout_secs: u64, /// Default exchange name pub exchange: String, } impl PublisherConfig { /// Get timeout as Duration pub fn timeout(&self) -> Duration { Duration::from_secs(self.timeout_secs) } } /// Consumer configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConsumerConfig { /// Queue name to consume from pub queue: String, /// Consumer tag (identifier) pub tag: String, /// Prefetch count (number of unacknowledged messages) #[serde(default = "default_prefetch_count")] pub prefetch_count: u16, /// Auto-acknowledge messages #[serde(default)] pub auto_ack: bool, /// Exclusive consumer #[serde(default)] pub exclusive: bool, } // Default value functions fn default_enabled() -> bool { true } fn default_true() -> bool { true } fn default_type() -> String { "rabbitmq".to_string() } fn default_host() -> String { "localhost".to_string() } fn default_port() -> u16 { 5672 } fn default_username() -> String { "guest".to_string() } fn default_password() -> String { "guest".to_string() } fn default_vhost() -> String { "/".to_string() } fn default_pool_size() -> usize { 10 } fn default_connection_timeout() -> u64 { 30 } fn default_heartbeat() -> u64 { 60 } fn default_reconnect_delay() -> u64 { 5 } fn default_max_reconnect_attempts() -> u32 { 10 } fn default_confirm_publish() -> bool { true } fn default_publish_timeout() -> u64 { 5 } fn default_prefetch_count() -> u16 { 10 } fn default_consumer_timeout() -> u64 { 300 } fn default_dlx_exchange() -> String { "attune.dlx".to_string() } fn default_dlq_ttl() -> u64 { 86400000 // 24 hours in milliseconds } fn default_worker_queue_ttl() -> u64 { 300000 // 5 minutes in milliseconds } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_config() { let config = MessageQueueConfig::default(); assert!(config.enabled); assert_eq!(config.r#type, "rabbitmq"); assert_eq!(config.rabbitmq.host, "localhost"); assert_eq!(config.rabbitmq.port, 5672); } #[test] fn test_connection_url() { let config = RabbitMqConfig::default(); let url = config.connection_url(); assert!(url.starts_with("amqp://")); assert!(url.contains("localhost")); assert!(url.contains("5672")); } #[test] fn test_validate() { let mut config = RabbitMqConfig::default(); assert!(config.validate().is_ok()); config.host = String::new(); assert!(config.validate().is_err()); config.host = "localhost".to_string(); config.pool_size = 0; assert!(config.validate().is_err()); } #[test] fn test_duration_conversions() { let config = RabbitMqConfig::default(); assert_eq!(config.connection_timeout().as_secs(), 30); assert_eq!(config.heartbeat().as_secs(), 60); assert_eq!(config.reconnect_delay().as_secs(), 5); } #[test] fn test_dead_letter_config() { let config = DeadLetterConfig::default(); assert!(config.enabled); assert_eq!(config.exchange, "attune.dlx"); assert_eq!(config.ttl().as_secs(), 86400); // 24 hours } #[test] fn test_worker_queue_ttl() { let config = RabbitMqConfig::default(); assert_eq!(config.worker_queue_ttl().as_secs(), 300); // 5 minutes assert_eq!(config.worker_queue_ttl_ms, 300000); } #[test] fn test_default_queues() { let queues = QueuesConfig::default(); assert_eq!(queues.events.name, "attune.events.queue"); assert_eq!(queues.executions.name, "attune.executions.queue"); assert_eq!( queues.execution_completed.name, "attune.execution.completed.queue" ); assert_eq!( queues.inquiry_responses.name, "attune.inquiry.responses.queue" ); assert_eq!(queues.notifications.name, "attune.notifications.queue"); assert!(queues.events.durable); } #[test] fn test_default_exchanges() { let exchanges = ExchangesConfig::default(); assert_eq!(exchanges.events.name, "attune.events"); assert_eq!(exchanges.executions.name, "attune.executions"); assert_eq!(exchanges.notifications.name, "attune.notifications"); assert!(matches!(exchanges.events.r#type, ExchangeType::Topic)); assert!(matches!(exchanges.executions.r#type, ExchangeType::Topic)); assert!(matches!( exchanges.notifications.r#type, ExchangeType::Fanout )); } }