1use serde::{Deserialize, Serialize};
7
8use crate::error::ValidationError;
9use crate::types::CollectiveId;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
17pub enum SyncDirection {
18 PushOnly,
20 PullOnly,
22 #[default]
24 Bidirectional,
25}
26
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub enum ConflictResolution {
35 #[default]
37 ServerWins,
38 LastWriteWins,
40}
41
42#[derive(Clone, Debug, Serialize, Deserialize)]
50pub struct RetryConfig {
51 pub max_retries: u32,
53
54 pub initial_backoff_ms: u64,
56
57 pub max_backoff_ms: u64,
59
60 pub backoff_multiplier: f64,
62}
63
64impl Default for RetryConfig {
65 fn default() -> Self {
66 Self {
67 max_retries: 5,
68 initial_backoff_ms: 500,
69 max_backoff_ms: 30_000,
70 backoff_multiplier: 2.0,
71 }
72 }
73}
74
75#[derive(Clone, Debug, Serialize, Deserialize)]
96pub struct SyncConfig {
97 pub direction: SyncDirection,
99
100 pub conflict_resolution: ConflictResolution,
102
103 pub batch_size: usize,
108
109 pub push_interval_ms: u64,
113
114 pub pull_interval_ms: u64,
118
119 pub retry: RetryConfig,
121
122 pub collectives: Option<Vec<CollectiveId>>,
126
127 pub sync_relations: bool,
131
132 pub sync_insights: bool,
136}
137
138impl Default for SyncConfig {
139 fn default() -> Self {
140 Self {
141 direction: SyncDirection::default(),
142 conflict_resolution: ConflictResolution::default(),
143 batch_size: 500,
144 push_interval_ms: 1000,
145 pull_interval_ms: 1000,
146 retry: RetryConfig::default(),
147 collectives: None,
148 sync_relations: true,
149 sync_insights: true,
150 }
151 }
152}
153
154impl SyncConfig {
155 pub fn validate(&self) -> Result<(), ValidationError> {
163 if self.batch_size == 0 {
164 return Err(ValidationError::invalid_field(
165 "batch_size",
166 "must be greater than 0",
167 ));
168 }
169 if self.push_interval_ms == 0 {
170 return Err(ValidationError::invalid_field(
171 "push_interval_ms",
172 "must be greater than 0",
173 ));
174 }
175 if self.pull_interval_ms == 0 {
176 return Err(ValidationError::invalid_field(
177 "pull_interval_ms",
178 "must be greater than 0",
179 ));
180 }
181 Ok(())
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_sync_config_defaults() {
191 let config = SyncConfig::default();
192 assert_eq!(config.direction, SyncDirection::Bidirectional);
193 assert_eq!(config.conflict_resolution, ConflictResolution::ServerWins);
194 assert_eq!(config.batch_size, 500);
195 assert_eq!(config.push_interval_ms, 1000);
196 assert_eq!(config.pull_interval_ms, 1000);
197 assert!(config.collectives.is_none());
198 assert!(config.sync_relations);
199 assert!(config.sync_insights);
200 }
201
202 #[test]
203 fn test_sync_config_validate_success() {
204 let config = SyncConfig::default();
205 assert!(config.validate().is_ok());
206 }
207
208 #[test]
209 fn test_sync_config_validate_zero_batch_size() {
210 let config = SyncConfig {
211 batch_size: 0,
212 ..Default::default()
213 };
214 let err = config.validate().unwrap_err();
215 assert!(
216 matches!(err, ValidationError::InvalidField { field, .. } if field == "batch_size")
217 );
218 }
219
220 #[test]
221 fn test_sync_config_validate_zero_push_interval() {
222 let config = SyncConfig {
223 push_interval_ms: 0,
224 ..Default::default()
225 };
226 let err = config.validate().unwrap_err();
227 assert!(
228 matches!(err, ValidationError::InvalidField { field, .. } if field == "push_interval_ms")
229 );
230 }
231
232 #[test]
233 fn test_sync_config_validate_zero_pull_interval() {
234 let config = SyncConfig {
235 pull_interval_ms: 0,
236 ..Default::default()
237 };
238 let err = config.validate().unwrap_err();
239 assert!(
240 matches!(err, ValidationError::InvalidField { field, .. } if field == "pull_interval_ms")
241 );
242 }
243
244 #[test]
245 fn test_sync_config_bincode_roundtrip() {
246 let config = SyncConfig {
247 direction: SyncDirection::PushOnly,
248 batch_size: 100,
249 collectives: Some(vec![CollectiveId::new()]),
250 ..Default::default()
251 };
252 let bytes = bincode::serialize(&config).unwrap();
253 let restored: SyncConfig = bincode::deserialize(&bytes).unwrap();
254 assert_eq!(config.direction, restored.direction);
255 assert_eq!(config.batch_size, restored.batch_size);
256 }
257
258 #[test]
259 fn test_retry_config_defaults() {
260 let config = RetryConfig::default();
261 assert_eq!(config.max_retries, 5);
262 assert_eq!(config.initial_backoff_ms, 500);
263 assert_eq!(config.max_backoff_ms, 30_000);
264 assert!((config.backoff_multiplier - 2.0).abs() < f64::EPSILON);
265 }
266}