Skip to main content

mabi_modbus/runtime/
updates.rs

1//! Configuration update utilities and helpers.
2
3use std::time::{Duration, SystemTime};
4
5use serde::{Deserialize, Serialize};
6
7use super::{ConfigUpdate, RuntimeState};
8
9/// Builder for creating configuration updates.
10#[derive(Default)]
11pub struct UpdateBuilder {
12    updates: Vec<ConfigUpdate>,
13}
14
15impl UpdateBuilder {
16    /// Create a new update builder.
17    pub fn new() -> Self {
18        Self::default()
19    }
20
21    /// Set maximum connections.
22    pub fn max_connections(mut self, max: usize) -> Self {
23        self.updates.push(ConfigUpdate::MaxConnections(max));
24        self
25    }
26
27    /// Set idle timeout.
28    pub fn idle_timeout(mut self, duration: Duration) -> Self {
29        self.updates.push(ConfigUpdate::IdleTimeout(duration));
30        self
31    }
32
33    /// Set request timeout.
34    pub fn request_timeout(mut self, duration: Duration) -> Self {
35        self.updates.push(ConfigUpdate::RequestTimeout(duration));
36        self
37    }
38
39    /// Enable/disable a unit.
40    pub fn unit_enabled(mut self, unit_id: u8, enabled: bool) -> Self {
41        self.updates
42            .push(ConfigUpdate::UnitEnabled { unit_id, enabled });
43        self
44    }
45
46    /// Enable TCP nodelay.
47    pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
48        self.updates.push(ConfigUpdate::TcpNoDelay(enabled));
49        self
50    }
51
52    /// Set keepalive interval.
53    pub fn keepalive(mut self, interval: Option<Duration>) -> Self {
54        self.updates.push(ConfigUpdate::KeepaliveInterval(interval));
55        self
56    }
57
58    /// Enable/disable metrics.
59    pub fn metrics_enabled(mut self, enabled: bool) -> Self {
60        self.updates.push(ConfigUpdate::MetricsEnabled(enabled));
61        self
62    }
63
64    /// Enable/disable debug logging.
65    pub fn debug_logging(mut self, enabled: bool) -> Self {
66        self.updates.push(ConfigUpdate::DebugLogging(enabled));
67        self
68    }
69
70    /// Set a register value.
71    pub fn set_register(mut self, unit_id: u8, address: u16, value: u16) -> Self {
72        self.updates.push(ConfigUpdate::SetRegister {
73            unit_id,
74            address,
75            value,
76        });
77        self
78    }
79
80    /// Set multiple registers.
81    pub fn set_registers(mut self, unit_id: u8, start_address: u16, values: Vec<u16>) -> Self {
82        self.updates.push(ConfigUpdate::SetRegisters {
83            unit_id,
84            start_address,
85            values,
86        });
87        self
88    }
89
90    /// Set a coil value.
91    pub fn set_coil(mut self, unit_id: u8, address: u16, value: bool) -> Self {
92        self.updates.push(ConfigUpdate::SetCoil {
93            unit_id,
94            address,
95            value,
96        });
97        self
98    }
99
100    /// Add a custom setting.
101    pub fn custom(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
102        self.updates.push(ConfigUpdate::Custom {
103            key: key.into(),
104            value: value.into(),
105        });
106        self
107    }
108
109    /// Add any update.
110    pub fn update(mut self, update: ConfigUpdate) -> Self {
111        self.updates.push(update);
112        self
113    }
114
115    /// Build the list of updates.
116    pub fn build(self) -> Vec<ConfigUpdate> {
117        self.updates
118    }
119
120    /// Get the number of updates.
121    pub fn len(&self) -> usize {
122        self.updates.len()
123    }
124
125    /// Check if empty.
126    pub fn is_empty(&self) -> bool {
127        self.updates.is_empty()
128    }
129}
130
131/// Record of a configuration update for auditing.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct UpdateRecord {
134    /// Unique ID for this update.
135    pub id: u64,
136
137    /// The update that was applied.
138    pub update: ConfigUpdate,
139
140    /// When the update was applied.
141    pub timestamp: SystemTime,
142
143    /// Whether the update was successful.
144    pub success: bool,
145
146    /// Error message if the update failed.
147    pub error: Option<String>,
148
149    /// Who initiated the update (if known).
150    pub source: Option<String>,
151}
152
153impl UpdateRecord {
154    /// Create a successful update record.
155    pub fn success(id: u64, update: ConfigUpdate) -> Self {
156        Self {
157            id,
158            update,
159            timestamp: SystemTime::now(),
160            success: true,
161            error: None,
162            source: None,
163        }
164    }
165
166    /// Create a failed update record.
167    pub fn failure(id: u64, update: ConfigUpdate, error: String) -> Self {
168        Self {
169            id,
170            update,
171            timestamp: SystemTime::now(),
172            success: false,
173            error: Some(error),
174            source: None,
175        }
176    }
177
178    /// Set the source of the update.
179    pub fn with_source(mut self, source: impl Into<String>) -> Self {
180        self.source = Some(source.into());
181        self
182    }
183}
184
185/// Audit log for configuration updates.
186pub struct UpdateAuditLog {
187    records: Vec<UpdateRecord>,
188    max_records: usize,
189    next_id: u64,
190}
191
192impl UpdateAuditLog {
193    /// Create a new audit log.
194    pub fn new(max_records: usize) -> Self {
195        Self {
196            records: Vec::with_capacity(max_records.min(1000)),
197            max_records,
198            next_id: 1,
199        }
200    }
201
202    /// Record a successful update.
203    pub fn record_success(&mut self, update: ConfigUpdate) -> &UpdateRecord {
204        let id = self.next_id;
205        self.next_id += 1;
206
207        self.add_record(UpdateRecord::success(id, update))
208    }
209
210    /// Record a failed update.
211    pub fn record_failure(&mut self, update: ConfigUpdate, error: String) -> &UpdateRecord {
212        let id = self.next_id;
213        self.next_id += 1;
214
215        self.add_record(UpdateRecord::failure(id, update, error))
216    }
217
218    fn add_record(&mut self, record: UpdateRecord) -> &UpdateRecord {
219        // Remove oldest if at capacity
220        while self.records.len() >= self.max_records {
221            self.records.remove(0);
222        }
223
224        self.records.push(record);
225        self.records.last().unwrap()
226    }
227
228    /// Get all records.
229    pub fn records(&self) -> &[UpdateRecord] {
230        &self.records
231    }
232
233    /// Get recent records (last N).
234    pub fn recent(&self, n: usize) -> &[UpdateRecord] {
235        let start = self.records.len().saturating_sub(n);
236        &self.records[start..]
237    }
238
239    /// Get failed updates only.
240    pub fn failures(&self) -> Vec<&UpdateRecord> {
241        self.records.iter().filter(|r| !r.success).collect()
242    }
243
244    /// Clear all records.
245    pub fn clear(&mut self) {
246        self.records.clear();
247    }
248
249    /// Get total update count.
250    pub fn total_count(&self) -> u64 {
251        self.next_id - 1
252    }
253
254    /// Get success rate.
255    pub fn success_rate(&self) -> f64 {
256        if self.records.is_empty() {
257            return 1.0;
258        }
259        let successes = self.records.iter().filter(|r| r.success).count();
260        successes as f64 / self.records.len() as f64
261    }
262}
263
264impl Default for UpdateAuditLog {
265    fn default() -> Self {
266        Self::new(1000)
267    }
268}
269
270/// Diff between two runtime states.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct StateDiff {
273    /// Fields that changed.
274    pub changes: Vec<FieldChange>,
275}
276
277/// A single field change.
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct FieldChange {
280    /// Field name.
281    pub field: String,
282    /// Old value (as string).
283    pub old: String,
284    /// New value (as string).
285    pub new: String,
286}
287
288impl StateDiff {
289    /// Calculate diff between two states.
290    pub fn diff(old: &RuntimeState, new: &RuntimeState) -> Self {
291        let mut changes = Vec::new();
292
293        if old.max_connections != new.max_connections {
294            changes.push(FieldChange {
295                field: "max_connections".into(),
296                old: old.max_connections.to_string(),
297                new: new.max_connections.to_string(),
298            });
299        }
300
301        if old.idle_timeout != new.idle_timeout {
302            changes.push(FieldChange {
303                field: "idle_timeout".into(),
304                old: format!("{:?}", old.idle_timeout),
305                new: format!("{:?}", new.idle_timeout),
306            });
307        }
308
309        if old.request_timeout != new.request_timeout {
310            changes.push(FieldChange {
311                field: "request_timeout".into(),
312                old: format!("{:?}", old.request_timeout),
313                new: format!("{:?}", new.request_timeout),
314            });
315        }
316
317        if old.tcp_nodelay != new.tcp_nodelay {
318            changes.push(FieldChange {
319                field: "tcp_nodelay".into(),
320                old: old.tcp_nodelay.to_string(),
321                new: new.tcp_nodelay.to_string(),
322            });
323        }
324
325        if old.metrics_enabled != new.metrics_enabled {
326            changes.push(FieldChange {
327                field: "metrics_enabled".into(),
328                old: old.metrics_enabled.to_string(),
329                new: new.metrics_enabled.to_string(),
330            });
331        }
332
333        if old.debug_logging != new.debug_logging {
334            changes.push(FieldChange {
335                field: "debug_logging".into(),
336                old: old.debug_logging.to_string(),
337                new: new.debug_logging.to_string(),
338            });
339        }
340
341        if old.enabled_units != new.enabled_units {
342            changes.push(FieldChange {
343                field: "enabled_units".into(),
344                old: format!("{:?}", old.enabled_units),
345                new: format!("{:?}", new.enabled_units),
346            });
347        }
348
349        Self { changes }
350    }
351
352    /// Check if there are any changes.
353    pub fn has_changes(&self) -> bool {
354        !self.changes.is_empty()
355    }
356
357    /// Get number of changes.
358    pub fn change_count(&self) -> usize {
359        self.changes.len()
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_update_builder() {
369        let updates = UpdateBuilder::new()
370            .max_connections(200)
371            .idle_timeout(Duration::from_secs(600))
372            .metrics_enabled(false)
373            .build();
374
375        assert_eq!(updates.len(), 3);
376    }
377
378    #[test]
379    fn test_update_builder_empty() {
380        let builder = UpdateBuilder::new();
381        assert!(builder.is_empty());
382    }
383
384    #[test]
385    fn test_update_builder_registers() {
386        let updates = UpdateBuilder::new()
387            .set_register(1, 100, 1234)
388            .set_registers(1, 200, vec![1, 2, 3])
389            .set_coil(1, 50, true)
390            .build();
391
392        assert_eq!(updates.len(), 3);
393    }
394
395    #[test]
396    fn test_update_record() {
397        let record = UpdateRecord::success(1, ConfigUpdate::MaxConnections(100))
398            .with_source("test_user");
399
400        assert!(record.success);
401        assert_eq!(record.source, Some("test_user".to_string()));
402    }
403
404    #[test]
405    fn test_update_record_failure() {
406        let record = UpdateRecord::failure(
407            2,
408            ConfigUpdate::MaxConnections(0),
409            "Invalid value".into(),
410        );
411
412        assert!(!record.success);
413        assert!(record.error.is_some());
414    }
415
416    #[test]
417    fn test_audit_log() {
418        let mut log = UpdateAuditLog::new(10);
419
420        log.record_success(ConfigUpdate::MaxConnections(100));
421        log.record_success(ConfigUpdate::MaxConnections(200));
422        log.record_failure(ConfigUpdate::MaxConnections(0), "Invalid".into());
423
424        assert_eq!(log.records().len(), 3);
425        assert_eq!(log.failures().len(), 1);
426        assert_eq!(log.total_count(), 3);
427    }
428
429    #[test]
430    fn test_audit_log_capacity() {
431        let mut log = UpdateAuditLog::new(5);
432
433        for i in 0..10 {
434            log.record_success(ConfigUpdate::MaxConnections(i));
435        }
436
437        assert_eq!(log.records().len(), 5);
438        assert_eq!(log.total_count(), 10);
439    }
440
441    #[test]
442    fn test_audit_log_success_rate() {
443        let mut log = UpdateAuditLog::new(10);
444
445        log.record_success(ConfigUpdate::MaxConnections(100));
446        log.record_success(ConfigUpdate::MaxConnections(100));
447        log.record_failure(ConfigUpdate::MaxConnections(0), "Error".into());
448
449        let rate = log.success_rate();
450        assert!((rate - 0.6666).abs() < 0.01);
451    }
452
453    #[test]
454    fn test_state_diff() {
455        let old = RuntimeState::default();
456        let mut new = RuntimeState::default();
457        new.max_connections = 200;
458        new.metrics_enabled = false;
459
460        let diff = StateDiff::diff(&old, &new);
461
462        assert!(diff.has_changes());
463        assert_eq!(diff.change_count(), 2);
464    }
465
466    #[test]
467    fn test_state_diff_no_changes() {
468        let state = RuntimeState::default();
469        let diff = StateDiff::diff(&state, &state);
470
471        assert!(!diff.has_changes());
472    }
473}