Skip to main content

canlink_hal/monitor/
config.rs

1//! Monitor configuration (FR-010)
2//!
3//! Provides configuration structures for loading monitor settings from TOML.
4
5use std::time::Duration;
6
7use serde::Deserialize;
8
9use super::{ConnectionMonitor, ReconnectConfig};
10
11/// Monitor configuration from TOML
12///
13/// # Example TOML
14///
15/// ```toml
16/// [monitor]
17/// heartbeat_interval_ms = 1000
18///
19/// # Optional: enable auto-reconnect
20/// [monitor.reconnect]
21/// max_retries = 5
22/// retry_interval_ms = 2000
23/// backoff_multiplier = 1.5
24/// ```
25#[derive(Debug, Clone, Deserialize)]
26pub struct MonitorConfig {
27    /// Heartbeat interval in milliseconds
28    #[serde(default = "default_heartbeat_ms")]
29    pub heartbeat_interval_ms: u64,
30
31    /// Reconnect configuration (optional)
32    pub reconnect: Option<ReconnectConfigFile>,
33}
34
35fn default_heartbeat_ms() -> u64 {
36    1000
37}
38
39impl Default for MonitorConfig {
40    fn default() -> Self {
41        Self {
42            heartbeat_interval_ms: default_heartbeat_ms(),
43            reconnect: None,
44        }
45    }
46}
47
48/// Reconnect configuration from TOML
49#[derive(Debug, Clone, Deserialize)]
50pub struct ReconnectConfigFile {
51    /// Maximum retries (0 = unlimited)
52    #[serde(default = "default_max_retries")]
53    pub max_retries: u32,
54
55    /// Retry interval in milliseconds
56    #[serde(default = "default_retry_interval_ms")]
57    pub retry_interval_ms: u64,
58
59    /// Backoff multiplier
60    #[serde(default = "default_backoff")]
61    pub backoff_multiplier: f32,
62}
63
64fn default_max_retries() -> u32 {
65    3
66}
67
68fn default_retry_interval_ms() -> u64 {
69    1000
70}
71
72fn default_backoff() -> f32 {
73    2.0
74}
75
76impl Default for ReconnectConfigFile {
77    fn default() -> Self {
78        Self {
79            max_retries: default_max_retries(),
80            retry_interval_ms: default_retry_interval_ms(),
81            backoff_multiplier: default_backoff(),
82        }
83    }
84}
85
86impl From<ReconnectConfigFile> for ReconnectConfig {
87    fn from(config: ReconnectConfigFile) -> Self {
88        ReconnectConfig {
89            max_retries: config.max_retries,
90            retry_interval: Duration::from_millis(config.retry_interval_ms),
91            backoff_multiplier: config.backoff_multiplier,
92        }
93    }
94}
95
96impl MonitorConfig {
97    /// Load configuration from TOML string
98    ///
99    /// # Errors
100    ///
101    /// Returns `toml::de::Error` if the TOML string is invalid or cannot be
102    /// deserialized into a `MonitorConfig`.
103    pub fn from_toml(toml_str: &str) -> Result<Self, toml::de::Error> {
104        toml::from_str(toml_str)
105    }
106
107    /// Create a `ConnectionMonitor` from this configuration
108    #[must_use]
109    pub fn into_monitor(self) -> ConnectionMonitor {
110        let heartbeat = Duration::from_millis(self.heartbeat_interval_ms);
111
112        if let Some(reconnect) = self.reconnect {
113            ConnectionMonitor::with_reconnect(heartbeat, reconnect.into())
114        } else {
115            ConnectionMonitor::new(heartbeat)
116        }
117    }
118}
119
120impl ConnectionMonitor {
121    /// Create a `ConnectionMonitor` from configuration
122    #[must_use]
123    pub fn from_config(config: &MonitorConfig) -> Self {
124        config.clone().into_monitor()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_default_config() {
134        let config = MonitorConfig::default();
135        assert_eq!(config.heartbeat_interval_ms, 1000);
136        assert!(config.reconnect.is_none());
137    }
138
139    #[test]
140    fn test_parse_basic() {
141        let toml = r"
142            heartbeat_interval_ms = 500
143        ";
144
145        let config: MonitorConfig = toml::from_str(toml).unwrap();
146        assert_eq!(config.heartbeat_interval_ms, 500);
147        assert!(config.reconnect.is_none());
148    }
149
150    #[test]
151    fn test_parse_with_reconnect() {
152        let toml = r"
153            heartbeat_interval_ms = 500
154
155            [reconnect]
156            max_retries = 5
157            retry_interval_ms = 2000
158            backoff_multiplier = 1.5
159        ";
160
161        let config: MonitorConfig = toml::from_str(toml).unwrap();
162        assert_eq!(config.heartbeat_interval_ms, 500);
163
164        let reconnect = config.reconnect.unwrap();
165        assert_eq!(reconnect.max_retries, 5);
166        assert_eq!(reconnect.retry_interval_ms, 2000);
167        assert!((reconnect.backoff_multiplier - 1.5).abs() < f32::EPSILON);
168    }
169
170    #[test]
171    fn test_into_monitor() {
172        let config = MonitorConfig {
173            heartbeat_interval_ms: 500,
174            reconnect: Some(ReconnectConfigFile::default()),
175        };
176
177        let monitor = config.into_monitor();
178        assert_eq!(monitor.heartbeat_interval(), Duration::from_millis(500));
179        assert!(monitor.auto_reconnect_enabled());
180    }
181
182    #[test]
183    fn test_into_monitor_no_reconnect() {
184        let config = MonitorConfig::default();
185        let monitor = config.into_monitor();
186        assert!(!monitor.auto_reconnect_enabled());
187    }
188}