1use std::fmt;
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct ConfigChange {
11 pub parameter: String,
13 pub current: Option<String>,
15 pub desired: String,
17 pub change_type: ChangeType,
19}
20
21#[derive(Debug, Clone, PartialEq)]
23pub enum ChangeType {
24 Add,
26 Modify,
28 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#[derive(Debug, Clone)]
72pub struct ConfigDiff {
73 changes: Vec<ConfigChange>,
74}
75
76impl ConfigDiff {
77 pub fn new() -> Self {
79 Self {
80 changes: Vec::new(),
81 }
82 }
83
84 pub fn add_change(&mut self, change: ConfigChange) {
86 self.changes.push(change);
87 }
88
89 pub fn has_changes(&self) -> bool {
91 !self.changes.is_empty()
92 }
93
94 pub fn changes(&self) -> &[ConfigChange] {
96 &self.changes
97 }
98
99 pub fn len(&self) -> usize {
101 self.changes.len()
102 }
103
104 pub fn is_empty(&self) -> bool {
106 self.changes.is_empty()
107 }
108
109 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 pub fn additions(&self) -> Vec<&ConfigChange> {
119 self.changes_by_type(ChangeType::Add)
120 }
121
122 pub fn modifications(&self) -> Vec<&ConfigChange> {
124 self.changes_by_type(ChangeType::Modify)
125 }
126
127 pub fn removals(&self) -> Vec<&ConfigChange> {
129 self.changes_by_type(ChangeType::Remove)
130 }
131
132 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}