chie_shared/config/diff.rs
1//! Configuration diff utilities
2
3use super::{FeatureFlags, NetworkConfig, RetryConfig};
4use serde::{Deserialize, Serialize};
5
6/// Represents a single configuration change with old and new values
7///
8/// This type is used to track individual field changes when comparing configurations,
9/// making it easy to log, audit, or display what changed during a configuration update.
10///
11/// # Examples
12///
13/// ```
14/// use chie_shared::ConfigChange;
15///
16/// // Track a simple numeric change
17/// let change = ConfigChange::new("max_connections", &100, &200);
18/// assert_eq!(change.field, "max_connections");
19/// assert_eq!(change.old_value, "100");
20/// assert_eq!(change.new_value, "200");
21///
22/// // Track a boolean change
23/// let change = ConfigChange::new("enable_relay", &true, &false);
24/// assert_eq!(change.old_value, "true");
25/// assert_eq!(change.new_value, "false");
26/// ```
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct ConfigChange {
29 /// Field name that changed
30 pub field: String,
31 /// Old value (serialized as string)
32 pub old_value: String,
33 /// New value (serialized as string)
34 pub new_value: String,
35}
36
37impl ConfigChange {
38 /// Create a new configuration change
39 #[must_use]
40 pub fn new(
41 field: impl Into<String>,
42 old_value: &impl ToString,
43 new_value: &impl ToString,
44 ) -> Self {
45 Self {
46 field: field.into(),
47 old_value: old_value.to_string(),
48 new_value: new_value.to_string(),
49 }
50 }
51}
52
53/// Configuration diff utilities for detecting differences between configurations
54///
55/// This utility provides methods to compare configuration instances and detect
56/// what fields have changed. Useful for configuration hot-reload, migration,
57/// auditing, and change tracking.
58///
59/// # Examples
60///
61/// ```
62/// use chie_shared::{ConfigDiff, RetryConfig};
63///
64/// let old_config = RetryConfig::default();
65/// let mut new_config = RetryConfig::default();
66/// new_config.max_attempts = 10;
67///
68/// let changes = ConfigDiff::retry_config(&old_config, &new_config);
69/// assert_eq!(changes.len(), 1);
70/// assert_eq!(changes[0].field, "max_attempts");
71/// assert_eq!(changes[0].new_value, "10");
72/// ```
73pub struct ConfigDiff;
74
75impl ConfigDiff {
76 /// Detect differences between two `NetworkConfig` instances
77 ///
78 /// Returns a list of changes between the old and new configurations.
79 /// Compares all fields including `max_connections`, timeouts, relay/DHT settings,
80 /// bootstrap peers, and listen addresses.
81 ///
82 /// # Examples
83 ///
84 /// ```
85 /// use chie_shared::{ConfigDiff, NetworkConfig};
86 ///
87 /// let old = NetworkConfig::default();
88 /// let mut new = NetworkConfig::default();
89 /// new.max_connections = 200;
90 /// new.enable_relay = false;
91 ///
92 /// let changes = ConfigDiff::network_config(&old, &new);
93 /// assert_eq!(changes.len(), 2);
94 ///
95 /// // Check for specific changes
96 /// let fields: Vec<&str> = changes.iter().map(|c| c.field.as_str()).collect();
97 /// assert!(fields.contains(&"max_connections"));
98 /// assert!(fields.contains(&"enable_relay"));
99 /// ```
100 #[must_use]
101 pub fn network_config(old: &NetworkConfig, new: &NetworkConfig) -> Vec<ConfigChange> {
102 let mut changes = Vec::new();
103
104 if old.max_connections != new.max_connections {
105 changes.push(ConfigChange::new(
106 "max_connections",
107 &old.max_connections,
108 &new.max_connections,
109 ));
110 }
111
112 if old.connection_timeout_ms != new.connection_timeout_ms {
113 changes.push(ConfigChange::new(
114 "connection_timeout_ms",
115 &old.connection_timeout_ms,
116 &new.connection_timeout_ms,
117 ));
118 }
119
120 if old.request_timeout_ms != new.request_timeout_ms {
121 changes.push(ConfigChange::new(
122 "request_timeout_ms",
123 &old.request_timeout_ms,
124 &new.request_timeout_ms,
125 ));
126 }
127
128 if old.enable_relay != new.enable_relay {
129 changes.push(ConfigChange::new(
130 "enable_relay",
131 &old.enable_relay,
132 &new.enable_relay,
133 ));
134 }
135
136 if old.enable_dht != new.enable_dht {
137 changes.push(ConfigChange::new(
138 "enable_dht",
139 &old.enable_dht,
140 &new.enable_dht,
141 ));
142 }
143
144 if old.bootstrap_peers != new.bootstrap_peers {
145 let old_val = format!("{:?}", old.bootstrap_peers);
146 let new_val = format!("{:?}", new.bootstrap_peers);
147 changes.push(ConfigChange::new("bootstrap_peers", &old_val, &new_val));
148 }
149
150 if old.listen_addrs != new.listen_addrs {
151 let old_val = format!("{:?}", old.listen_addrs);
152 let new_val = format!("{:?}", new.listen_addrs);
153 changes.push(ConfigChange::new("listen_addrs", &old_val, &new_val));
154 }
155
156 changes
157 }
158
159 /// Detect differences between two `RetryConfig` instances
160 ///
161 /// Returns a list of changes between the old and new configurations.
162 ///
163 /// # Examples
164 ///
165 /// ```
166 /// use chie_shared::{ConfigDiff, RetryConfigBuilder};
167 ///
168 /// let old = RetryConfigBuilder::new()
169 /// .max_attempts(3)
170 /// .initial_backoff_ms(100)
171 /// .build();
172 ///
173 /// let new = RetryConfigBuilder::new()
174 /// .max_attempts(5)
175 /// .initial_backoff_ms(200)
176 /// .build();
177 ///
178 /// let changes = ConfigDiff::retry_config(&old, &new);
179 /// assert_eq!(changes.len(), 2);
180 ///
181 /// // Verify specific changes
182 /// let max_attempts_change = changes.iter()
183 /// .find(|c| c.field == "max_attempts")
184 /// .unwrap();
185 /// assert_eq!(max_attempts_change.old_value, "3");
186 /// assert_eq!(max_attempts_change.new_value, "5");
187 /// ```
188 #[must_use]
189 pub fn retry_config(old: &RetryConfig, new: &RetryConfig) -> Vec<ConfigChange> {
190 let mut changes = Vec::new();
191
192 if old.max_attempts != new.max_attempts {
193 changes.push(ConfigChange::new(
194 "max_attempts",
195 &old.max_attempts,
196 &new.max_attempts,
197 ));
198 }
199
200 if old.initial_backoff_ms != new.initial_backoff_ms {
201 changes.push(ConfigChange::new(
202 "initial_backoff_ms",
203 &old.initial_backoff_ms,
204 &new.initial_backoff_ms,
205 ));
206 }
207
208 if old.max_backoff_ms != new.max_backoff_ms {
209 changes.push(ConfigChange::new(
210 "max_backoff_ms",
211 &old.max_backoff_ms,
212 &new.max_backoff_ms,
213 ));
214 }
215
216 #[allow(clippy::float_cmp)]
217 if old.multiplier != new.multiplier {
218 changes.push(ConfigChange::new(
219 "multiplier",
220 &old.multiplier,
221 &new.multiplier,
222 ));
223 }
224
225 if old.enable_jitter != new.enable_jitter {
226 changes.push(ConfigChange::new(
227 "enable_jitter",
228 &old.enable_jitter,
229 &new.enable_jitter,
230 ));
231 }
232
233 changes
234 }
235
236 /// Detect differences between two `FeatureFlags` instances
237 ///
238 /// Returns a list of changes between the old and new configurations.
239 ///
240 /// # Examples
241 ///
242 /// ```
243 /// use chie_shared::{ConfigDiff, FeatureFlagsBuilder};
244 ///
245 /// let old = FeatureFlagsBuilder::new()
246 /// .experimental(false)
247 /// .debug_mode(false)
248 /// .build();
249 ///
250 /// let new = FeatureFlagsBuilder::new()
251 /// .experimental(true)
252 /// .debug_mode(true)
253 /// .performance_profiling(true)
254 /// .build();
255 ///
256 /// let changes = ConfigDiff::feature_flags(&old, &new);
257 /// assert!(changes.len() >= 2); // At least experimental and debug_mode changed
258 ///
259 /// // Check for experimental flag change
260 /// let exp_change = changes.iter()
261 /// .find(|c| c.field == "experimental")
262 /// .unwrap();
263 /// assert_eq!(exp_change.old_value, "false");
264 /// assert_eq!(exp_change.new_value, "true");
265 /// ```
266 #[must_use]
267 pub fn feature_flags(old: &FeatureFlags, new: &FeatureFlags) -> Vec<ConfigChange> {
268 let mut changes = Vec::new();
269
270 if old.experimental != new.experimental {
271 changes.push(ConfigChange::new(
272 "experimental",
273 &old.experimental,
274 &new.experimental,
275 ));
276 }
277
278 if old.beta != new.beta {
279 changes.push(ConfigChange::new("beta", &old.beta, &new.beta));
280 }
281
282 if old.enhanced_telemetry != new.enhanced_telemetry {
283 changes.push(ConfigChange::new(
284 "enhanced_telemetry",
285 &old.enhanced_telemetry,
286 &new.enhanced_telemetry,
287 ));
288 }
289
290 if old.performance_profiling != new.performance_profiling {
291 changes.push(ConfigChange::new(
292 "performance_profiling",
293 &old.performance_profiling,
294 &new.performance_profiling,
295 ));
296 }
297
298 if old.debug_mode != new.debug_mode {
299 changes.push(ConfigChange::new(
300 "debug_mode",
301 &old.debug_mode,
302 &new.debug_mode,
303 ));
304 }
305
306 if old.compression_optimization != new.compression_optimization {
307 changes.push(ConfigChange::new(
308 "compression_optimization",
309 &old.compression_optimization,
310 &new.compression_optimization,
311 ));
312 }
313
314 if old.adaptive_retry != new.adaptive_retry {
315 changes.push(ConfigChange::new(
316 "adaptive_retry",
317 &old.adaptive_retry,
318 &new.adaptive_retry,
319 ));
320 }
321
322 changes
323 }
324}