use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HorizonConfig {
pub enabled: bool,
pub hub_url: String,
pub api_key: String,
pub sensor_id: String,
pub sensor_name: Option<String>,
pub version: String,
pub reconnect_delay_ms: u64,
pub max_reconnect_attempts: u32,
#[serde(default = "default_circuit_breaker_threshold")]
pub circuit_breaker_threshold: u32,
#[serde(default = "default_circuit_breaker_cooldown_ms")]
pub circuit_breaker_cooldown_ms: u64,
pub signal_batch_size: usize,
pub signal_batch_delay_ms: u64,
pub heartbeat_interval_ms: u64,
pub max_queued_signals: usize,
pub blocklist_cache_ttl_secs: u64,
}
impl Default for HorizonConfig {
fn default() -> Self {
Self {
enabled: false,
hub_url: String::new(),
api_key: String::new(),
sensor_id: String::new(),
sensor_name: None,
version: env!("CARGO_PKG_VERSION").to_string(),
reconnect_delay_ms: 5_000,
max_reconnect_attempts: 0, circuit_breaker_threshold: default_circuit_breaker_threshold(),
circuit_breaker_cooldown_ms: default_circuit_breaker_cooldown_ms(),
signal_batch_size: 100,
signal_batch_delay_ms: 1_000,
heartbeat_interval_ms: 30_000,
max_queued_signals: 1_000,
blocklist_cache_ttl_secs: 3_600,
}
}
}
impl HorizonConfig {
pub fn with_hub_url(mut self, url: &str) -> Self {
self.hub_url = url.to_string();
self.enabled = !url.is_empty();
self
}
pub fn with_api_key(mut self, key: &str) -> Self {
self.api_key = key.to_string();
self
}
pub fn with_sensor_id(mut self, id: &str) -> Self {
self.sensor_id = id.to_string();
self
}
pub fn with_sensor_name(mut self, name: &str) -> Self {
self.sensor_name = Some(name.to_string());
self
}
pub fn with_version(mut self, version: &str) -> Self {
self.version = version.to_string();
self
}
pub fn with_reconnect_delay_ms(mut self, delay: u64) -> Self {
self.reconnect_delay_ms = delay;
self
}
pub fn with_batch_size(mut self, size: usize) -> Self {
self.signal_batch_size = size;
self
}
pub fn with_heartbeat_interval_ms(mut self, interval: u64) -> Self {
self.heartbeat_interval_ms = interval;
self
}
pub fn validate(&self) -> Result<(), super::error::HorizonError> {
if self.enabled {
if self.hub_url.is_empty() {
return Err(super::error::HorizonError::ConfigError(
"hub_url is required when enabled".to_string(),
));
}
if self.api_key.is_empty() {
return Err(super::error::HorizonError::ConfigError(
"api_key is required when enabled".to_string(),
));
}
if self.sensor_id.is_empty() {
return Err(super::error::HorizonError::ConfigError(
"sensor_id is required when enabled".to_string(),
));
}
}
Ok(())
}
}
fn default_circuit_breaker_threshold() -> u32 {
5
}
fn default_circuit_breaker_cooldown_ms() -> u64 {
300_000
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = HorizonConfig::default();
assert!(!config.enabled);
assert!(config.hub_url.is_empty());
}
#[test]
fn test_builder_pattern() {
let config = HorizonConfig::default()
.with_hub_url("wss://example.com/ws")
.with_api_key("test-key")
.with_sensor_id("sensor-1");
assert!(config.enabled);
assert_eq!(config.hub_url, "wss://example.com/ws");
assert_eq!(config.api_key, "test-key");
assert_eq!(config.sensor_id, "sensor-1");
}
#[test]
fn test_validation() {
let config = HorizonConfig::default();
assert!(config.validate().is_ok());
let config = HorizonConfig::default().with_hub_url("wss://example.com");
assert!(config.validate().is_err());
let config = HorizonConfig::default()
.with_hub_url("wss://example.com")
.with_api_key("key")
.with_sensor_id("sensor");
assert!(config.validate().is_ok());
}
}