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}