lmrc_postgres/
diff.rs

1//! Configuration diff detection
2//!
3//! This module provides functionality to detect and track configuration changes
4//! between desired and current PostgreSQL configurations.
5
6use std::fmt;
7
8/// Represents a single configuration change
9#[derive(Debug, Clone, PartialEq)]
10pub struct ConfigChange {
11    /// Configuration parameter name
12    pub parameter: String,
13    /// Current value on the server (None if not set)
14    pub current: Option<String>,
15    /// Desired value from configuration
16    pub desired: String,
17    /// Type of change
18    pub change_type: ChangeType,
19}
20
21/// Type of configuration change
22#[derive(Debug, Clone, PartialEq)]
23pub enum ChangeType {
24    /// Parameter needs to be added
25    Add,
26    /// Parameter needs to be modified
27    Modify,
28    /// Parameter needs to be removed
29    Remove,
30}
31
32impl fmt::Display for ConfigChange {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self.change_type {
35            ChangeType::Add => write!(f, "+ {}: {}", self.parameter, self.desired),
36            ChangeType::Modify => write!(
37                f,
38                "~ {}: {} -> {}",
39                self.parameter,
40                self.current.as_deref().unwrap_or("(unset)"),
41                self.desired
42            ),
43            ChangeType::Remove => write!(
44                f,
45                "- {}: {}",
46                self.parameter,
47                self.current.as_deref().unwrap_or("(unset)")
48            ),
49        }
50    }
51}
52
53/// Configuration diff result
54///
55/// Contains all detected changes between desired and current configuration.
56///
57/// # Example
58///
59/// ```rust
60/// use lmrc_postgres::ConfigDiff;
61///
62/// # fn example(diff: ConfigDiff) {
63/// if diff.has_changes() {
64///     println!("Configuration changes detected:");
65///     for change in diff.changes() {
66///         println!("  {}", change);
67///     }
68/// }
69/// # }
70/// ```
71#[derive(Debug, Clone)]
72pub struct ConfigDiff {
73    changes: Vec<ConfigChange>,
74}
75
76impl ConfigDiff {
77    /// Create a new empty diff
78    pub fn new() -> Self {
79        Self {
80            changes: Vec::new(),
81        }
82    }
83
84    /// Add a change to the diff
85    pub fn add_change(&mut self, change: ConfigChange) {
86        self.changes.push(change);
87    }
88
89    /// Check if there are any changes
90    pub fn has_changes(&self) -> bool {
91        !self.changes.is_empty()
92    }
93
94    /// Get all changes
95    pub fn changes(&self) -> &[ConfigChange] {
96        &self.changes
97    }
98
99    /// Get number of changes
100    pub fn len(&self) -> usize {
101        self.changes.len()
102    }
103
104    /// Check if diff is empty
105    pub fn is_empty(&self) -> bool {
106        self.changes.is_empty()
107    }
108
109    /// Get changes by type
110    pub fn changes_by_type(&self, change_type: ChangeType) -> Vec<&ConfigChange> {
111        self.changes
112            .iter()
113            .filter(|c| c.change_type == change_type)
114            .collect()
115    }
116
117    /// Get additions
118    pub fn additions(&self) -> Vec<&ConfigChange> {
119        self.changes_by_type(ChangeType::Add)
120    }
121
122    /// Get modifications
123    pub fn modifications(&self) -> Vec<&ConfigChange> {
124        self.changes_by_type(ChangeType::Modify)
125    }
126
127    /// Get removals
128    pub fn removals(&self) -> Vec<&ConfigChange> {
129        self.changes_by_type(ChangeType::Remove)
130    }
131
132    /// Create a summary string
133    pub fn summary(&self) -> String {
134        if self.is_empty() {
135            return "No changes".to_string();
136        }
137
138        let adds = self.additions().len();
139        let mods = self.modifications().len();
140        let rems = self.removals().len();
141
142        let mut parts = Vec::new();
143        if adds > 0 {
144            parts.push(format!(
145                "{} addition{}",
146                adds,
147                if adds == 1 { "" } else { "s" }
148            ));
149        }
150        if mods > 0 {
151            parts.push(format!(
152                "{} modification{}",
153                mods,
154                if mods == 1 { "" } else { "s" }
155            ));
156        }
157        if rems > 0 {
158            parts.push(format!(
159                "{} removal{}",
160                rems,
161                if rems == 1 { "" } else { "s" }
162            ));
163        }
164
165        parts.join(", ")
166    }
167}
168
169impl Default for ConfigDiff {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175impl fmt::Display for ConfigDiff {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        if self.is_empty() {
178            write!(f, "No configuration changes")
179        } else {
180            writeln!(f, "Configuration changes ({}):", self.summary())?;
181            for change in &self.changes {
182                writeln!(f, "  {}", change)?;
183            }
184            Ok(())
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_config_change_display() {
195        let change = ConfigChange {
196            parameter: "max_connections".to_string(),
197            current: Some("100".to_string()),
198            desired: "200".to_string(),
199            change_type: ChangeType::Modify,
200        };
201
202        assert_eq!(change.to_string(), "~ max_connections: 100 -> 200");
203
204        let change = ConfigChange {
205            parameter: "shared_buffers".to_string(),
206            current: None,
207            desired: "256MB".to_string(),
208            change_type: ChangeType::Add,
209        };
210
211        assert_eq!(change.to_string(), "+ shared_buffers: 256MB");
212    }
213
214    #[test]
215    fn test_config_diff() {
216        let mut diff = ConfigDiff::new();
217        assert!(!diff.has_changes());
218        assert_eq!(diff.len(), 0);
219
220        diff.add_change(ConfigChange {
221            parameter: "max_connections".to_string(),
222            current: Some("100".to_string()),
223            desired: "200".to_string(),
224            change_type: ChangeType::Modify,
225        });
226
227        diff.add_change(ConfigChange {
228            parameter: "shared_buffers".to_string(),
229            current: None,
230            desired: "256MB".to_string(),
231            change_type: ChangeType::Add,
232        });
233
234        assert!(diff.has_changes());
235        assert_eq!(diff.len(), 2);
236        assert_eq!(diff.additions().len(), 1);
237        assert_eq!(diff.modifications().len(), 1);
238        assert_eq!(diff.removals().len(), 0);
239    }
240
241    #[test]
242    fn test_diff_summary() {
243        let mut diff = ConfigDiff::new();
244        assert_eq!(diff.summary(), "No changes");
245
246        diff.add_change(ConfigChange {
247            parameter: "test".to_string(),
248            current: None,
249            desired: "value".to_string(),
250            change_type: ChangeType::Add,
251        });
252
253        assert_eq!(diff.summary(), "1 addition");
254
255        diff.add_change(ConfigChange {
256            parameter: "test2".to_string(),
257            current: Some("old".to_string()),
258            desired: "new".to_string(),
259            change_type: ChangeType::Modify,
260        });
261
262        assert_eq!(diff.summary(), "1 addition, 1 modification");
263    }
264
265    #[test]
266    fn test_diff_display() {
267        let mut diff = ConfigDiff::new();
268        diff.add_change(ConfigChange {
269            parameter: "max_connections".to_string(),
270            current: Some("100".to_string()),
271            desired: "200".to_string(),
272            change_type: ChangeType::Modify,
273        });
274
275        let display = diff.to_string();
276        assert!(display.contains("Configuration changes"));
277        assert!(display.contains("max_connections"));
278    }
279}