//! Configuration module for timer sensor //! //! Supports loading configuration from environment variables or stdin JSON. use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::io::Read; /// Sensor configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SensorConfig { /// Base URL of the Attune API pub api_url: String, /// API token for authentication pub api_token: String, /// Sensor reference name (e.g., "core.timer") pub sensor_ref: String, /// RabbitMQ connection URL pub mq_url: String, /// RabbitMQ exchange name (default: "attune") #[serde(default = "default_exchange")] pub mq_exchange: String, /// Log level (default: "info") #[serde(default = "default_log_level")] pub log_level: String, } fn default_exchange() -> String { "attune".to_string() } fn default_log_level() -> String { "info".to_string() } impl SensorConfig { /// Load configuration from environment variables pub fn from_env() -> Result { let api_url = std::env::var("ATTUNE_API_URL") .context("ATTUNE_API_URL environment variable is required")?; let api_token = std::env::var("ATTUNE_API_TOKEN") .context("ATTUNE_API_TOKEN environment variable is required")?; let sensor_ref = std::env::var("ATTUNE_SENSOR_REF") .context("ATTUNE_SENSOR_REF environment variable is required")?; let mq_url = std::env::var("ATTUNE_MQ_URL") .context("ATTUNE_MQ_URL environment variable is required")?; let mq_exchange = std::env::var("ATTUNE_MQ_EXCHANGE").unwrap_or_else(|_| default_exchange()); let log_level = std::env::var("ATTUNE_LOG_LEVEL").unwrap_or_else(|_| default_log_level()); Ok(Self { api_url, api_token, sensor_ref, mq_url, mq_exchange, log_level, }) } /// Load configuration from stdin JSON pub async fn from_stdin() -> Result { let mut buffer = String::new(); std::io::stdin() .read_to_string(&mut buffer) .context("Failed to read configuration from stdin")?; serde_json::from_str(&buffer).context("Failed to parse JSON configuration from stdin") } /// Validate configuration pub fn validate(&self) -> Result<()> { if self.api_url.is_empty() { return Err(anyhow::anyhow!("api_url cannot be empty")); } if self.api_token.is_empty() { return Err(anyhow::anyhow!("api_token cannot be empty")); } if self.sensor_ref.is_empty() { return Err(anyhow::anyhow!("sensor_ref cannot be empty")); } if self.mq_url.is_empty() { return Err(anyhow::anyhow!("mq_url cannot be empty")); } if self.mq_exchange.is_empty() { return Err(anyhow::anyhow!("mq_exchange cannot be empty")); } // Validate API URL format if !self.api_url.starts_with("http://") && !self.api_url.starts_with("https://") { return Err(anyhow::anyhow!( "api_url must start with http:// or https://" )); } // Validate MQ URL format if !self.mq_url.starts_with("amqp://") && !self.mq_url.starts_with("amqps://") { return Err(anyhow::anyhow!( "mq_url must start with amqp:// or amqps://" )); } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_validation() { let config = SensorConfig { api_url: "http://localhost:8080".to_string(), api_token: "test_token".to_string(), sensor_ref: "core.timer".to_string(), mq_url: "amqp://localhost:5672".to_string(), mq_exchange: "attune".to_string(), log_level: "info".to_string(), }; assert!(config.validate().is_ok()); } #[test] fn test_config_validation_invalid_api_url() { let config = SensorConfig { api_url: "localhost:8080".to_string(), // Missing http:// api_token: "test_token".to_string(), sensor_ref: "core.timer".to_string(), mq_url: "amqp://localhost:5672".to_string(), mq_exchange: "attune".to_string(), log_level: "info".to_string(), }; assert!(config.validate().is_err()); } #[test] fn test_config_validation_invalid_mq_url() { let config = SensorConfig { api_url: "http://localhost:8080".to_string(), api_token: "test_token".to_string(), sensor_ref: "core.timer".to_string(), mq_url: "localhost:5672".to_string(), // Missing amqp:// mq_exchange: "attune".to_string(), log_level: "info".to_string(), }; assert!(config.validate().is_err()); } #[test] fn test_config_deserialization() { let json = r#"{ "api_url": "http://localhost:8080", "api_token": "test_token", "sensor_ref": "core.timer", "mq_url": "amqp://localhost:5672" }"#; let config: SensorConfig = serde_json::from_str(json).unwrap(); assert_eq!(config.api_url, "http://localhost:8080"); assert_eq!(config.api_token, "test_token"); assert_eq!(config.sensor_ref, "core.timer"); assert_eq!(config.mq_url, "amqp://localhost:5672"); assert_eq!(config.mq_exchange, "attune"); // Default assert_eq!(config.log_level, "info"); // Default } #[test] fn test_config_deserialization_with_optionals() { let json = r#"{ "api_url": "http://localhost:8080", "api_token": "test_token", "sensor_ref": "core.timer", "mq_url": "amqp://localhost:5672", "mq_exchange": "custom", "log_level": "debug" }"#; let config: SensorConfig = serde_json::from_str(json).unwrap(); assert_eq!(config.mq_exchange, "custom"); assert_eq!(config.log_level, "debug"); } }