1use std::{collections::HashMap, time::Duration};
43
44use serde::{Deserialize, Serialize};
45
46use crate::domain::resilience::{BackoffStrategy, ResiliencePolicy};
47
48#[derive(Clone, Debug, Default, Serialize, Deserialize)]
50pub struct ResilienceConfig {
51 #[serde(default = "default_enabled")]
53 pub enabled: bool,
54
55 #[serde(default)]
57 pub policies: HashMap<String, PolicyConfig>,
58
59 #[serde(default)]
61 pub services: HashMap<String, String>,
62}
63
64fn default_enabled() -> bool {
65 true
66}
67
68#[derive(Clone, Debug, Serialize, Deserialize)]
70#[serde(untagged)]
71pub enum PolicyConfig {
72 Simple(SimplePolicyConfig),
74
75 Combined {
77 retry: Option<RetryConfig>,
79
80 circuit_breaker: Option<CircuitBreakerConfig>,
82
83 rate_limit: Option<RateLimitConfig>,
85
86 timeout: Option<TimeoutConfig>,
88 },
89}
90
91#[derive(Clone, Debug, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case")]
94pub enum SimplePolicyConfig {
95 None,
97
98 Retry(RetryConfig),
100
101 CircuitBreaker(CircuitBreakerConfig),
103
104 RateLimit(RateLimitConfig),
106
107 Timeout(TimeoutConfig),
109}
110
111#[derive(Clone, Debug, Serialize, Deserialize)]
113pub struct RetryConfig {
114 pub max_attempts: u32,
116
117 #[serde(flatten)]
119 pub backoff: BackoffConfig,
120}
121
122#[derive(Clone, Debug, Serialize, Deserialize)]
124#[serde(tag = "backoff_type", rename_all = "snake_case")]
125pub enum BackoffConfig {
126 Fixed {
128 delay_ms: u64,
130 },
131
132 Exponential {
134 initial_delay_ms: u64,
136
137 #[serde(default = "default_multiplier")]
139 multiplier: f64,
140
141 max_delay_ms: Option<u64>,
143
144 #[serde(default = "default_jitter")]
146 jitter: bool,
147 },
148
149 Linear {
151 initial_delay_ms: u64,
153
154 increment_ms: u64,
156
157 max_delay_ms: Option<u64>,
159 },
160}
161
162fn default_multiplier() -> f64 {
163 2.0
164}
165
166fn default_jitter() -> bool {
167 true
168}
169
170#[derive(Clone, Debug, Serialize, Deserialize)]
172pub struct CircuitBreakerConfig {
173 pub failure_threshold: u32,
175
176 pub recovery_timeout_seconds: u64,
178
179 #[serde(default = "default_success_threshold")]
181 pub success_threshold: u32,
182}
183
184fn default_success_threshold() -> u32 {
185 3
186}
187
188#[derive(Clone, Debug, Serialize, Deserialize)]
190pub struct RateLimitConfig {
191 pub requests_per_second: u32,
193
194 #[serde(default = "default_burst_capacity")]
196 pub burst_capacity: u32,
197}
198
199fn default_burst_capacity() -> u32 {
200 10
201}
202
203#[derive(Clone, Debug, Serialize, Deserialize)]
205pub struct TimeoutConfig {
206 pub duration_seconds: u64,
208}
209
210impl ResilienceConfig {
211 pub fn from_toml(content: &str) -> Result<Self, ResilienceConfigError> {
213 toml::from_str(content).map_err(|e| ResilienceConfigError::Toml(e.to_string()))
214 }
215
216 pub fn from_yaml(_content: &str) -> Result<Self, ResilienceConfigError> {
218 Err(ResilienceConfigError::Yaml(
220 "YAML support not available".to_string(),
221 ))
222 }
223
224 pub fn from_file(path: &std::path::Path) -> Result<Self, ResilienceConfigError> {
227 let content =
228 std::fs::read_to_string(path).map_err(|e| ResilienceConfigError::Io(e.to_string()))?;
229
230 match path.extension().and_then(|s| s.to_str()) {
231 Some("toml") => Self::from_toml(&content),
232 Some("yaml") | Some("yml") => Self::from_yaml(&content),
233 _ => Err(ResilienceConfigError::UnsupportedFormat(
234 path.display().to_string(),
235 )),
236 }
237 }
238
239 pub fn get_policy_for_service(&self, service_name: &str) -> Option<ResiliencePolicy> {
241 if !self.enabled {
242 return Some(ResiliencePolicy::None);
243 }
244
245 let policy_name = self.services.get(service_name)?;
247
248 let policy_config = self.policies.get(policy_name)?;
250
251 Some(policy_config.to_policy())
252 }
253
254 pub fn get_policy(&self, policy_name: &str) -> Option<ResiliencePolicy> {
256 if !self.enabled {
257 return Some(ResiliencePolicy::None);
258 }
259
260 self.policies
261 .get(policy_name)
262 .map(|config| config.to_policy())
263 }
264
265 pub fn get_default_policy(&self) -> ResiliencePolicy {
267 if !self.enabled {
268 return ResiliencePolicy::None;
269 }
270
271 self.policies
272 .get("default")
273 .map(|config| config.to_policy())
274 .unwrap_or(ResiliencePolicy::None)
275 }
276}
277
278impl PolicyConfig {
279 pub fn to_policy(&self) -> ResiliencePolicy {
281 match self {
282 PolicyConfig::Simple(simple) => match simple {
283 SimplePolicyConfig::None => ResiliencePolicy::None,
284 SimplePolicyConfig::Retry(config) => ResiliencePolicy::Retry {
285 max_attempts: config.max_attempts,
286 backoff: config.backoff.to_backoff_strategy(),
287 },
288 SimplePolicyConfig::CircuitBreaker(config) => ResiliencePolicy::CircuitBreaker {
289 failure_threshold: config.failure_threshold,
290 recovery_timeout: Duration::from_secs(config.recovery_timeout_seconds),
291 success_threshold: config.success_threshold,
292 },
293 SimplePolicyConfig::RateLimit(config) => ResiliencePolicy::RateLimit {
294 requests_per_second: config.requests_per_second,
295 burst_capacity: config.burst_capacity,
296 },
297 SimplePolicyConfig::Timeout(config) => ResiliencePolicy::Timeout {
298 duration: Duration::from_secs(config.duration_seconds),
299 },
300 },
301
302 PolicyConfig::Combined {
303 retry,
304 circuit_breaker,
305 rate_limit,
306 timeout,
307 } => {
308 let mut policies = Vec::new();
309
310 if let Some(config) = retry {
311 policies.push(ResiliencePolicy::Retry {
312 max_attempts: config.max_attempts,
313 backoff: config.backoff.to_backoff_strategy(),
314 });
315 }
316
317 if let Some(config) = circuit_breaker {
318 policies.push(ResiliencePolicy::CircuitBreaker {
319 failure_threshold: config.failure_threshold,
320 recovery_timeout: Duration::from_secs(config.recovery_timeout_seconds),
321 success_threshold: config.success_threshold,
322 });
323 }
324
325 if let Some(config) = rate_limit {
326 policies.push(ResiliencePolicy::RateLimit {
327 requests_per_second: config.requests_per_second,
328 burst_capacity: config.burst_capacity,
329 });
330 }
331
332 if let Some(config) = timeout {
333 policies.push(ResiliencePolicy::Timeout {
334 duration: Duration::from_secs(config.duration_seconds),
335 });
336 }
337
338 match policies.len() {
339 0 => ResiliencePolicy::None,
340 1 => policies.into_iter().next().unwrap(),
341 _ => ResiliencePolicy::Combined { policies },
342 }
343 }
344 }
345 }
346}
347
348impl BackoffConfig {
349 pub fn to_backoff_strategy(&self) -> BackoffStrategy {
351 match self {
352 BackoffConfig::Fixed { delay_ms } => BackoffStrategy::Fixed {
353 delay: Duration::from_millis(*delay_ms),
354 },
355
356 BackoffConfig::Exponential {
357 initial_delay_ms,
358 multiplier,
359 max_delay_ms,
360 jitter,
361 } => BackoffStrategy::Exponential {
362 initial_delay: Duration::from_millis(*initial_delay_ms),
363 multiplier: *multiplier,
364 max_delay: max_delay_ms.map(Duration::from_millis),
365 jitter: *jitter,
366 },
367
368 BackoffConfig::Linear {
369 initial_delay_ms,
370 increment_ms,
371 max_delay_ms,
372 } => BackoffStrategy::Linear {
373 initial_delay: Duration::from_millis(*initial_delay_ms),
374 increment: Duration::from_millis(*increment_ms),
375 max_delay: max_delay_ms.map(Duration::from_millis),
376 },
377 }
378 }
379}
380
381#[derive(thiserror::Error, Debug)]
383pub enum ResilienceConfigError {
384 #[error("IO error: {0}")]
385 Io(String),
386
387 #[error("TOML parsing error: {0}")]
388 Toml(String),
389
390 #[error("YAML parsing error: {0}")]
391 Yaml(String),
392
393 #[error("Unsupported file format: {0}")]
394 UnsupportedFormat(String),
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_simple_retry_policy_config() {
403 let config = PolicyConfig::Simple(SimplePolicyConfig::Retry(RetryConfig {
404 max_attempts: 3,
405 backoff: BackoffConfig::Exponential {
406 initial_delay_ms: 100,
407 multiplier: 2.0,
408 max_delay_ms: Some(10000),
409 jitter: true,
410 },
411 }));
412
413 let policy = config.to_policy();
414 match policy {
415 ResiliencePolicy::Retry {
416 max_attempts,
417 backoff: BackoffStrategy::Exponential { .. },
418 } => {
419 assert_eq!(max_attempts, 3);
420 }
421 _ => panic!("Expected retry policy"),
422 }
423 }
424
425 #[test]
426 fn test_combined_policy_config() {
427 let config = PolicyConfig::Combined {
428 retry: Some(RetryConfig {
429 max_attempts: 3,
430 backoff: BackoffConfig::Fixed { delay_ms: 1000 },
431 }),
432 circuit_breaker: Some(CircuitBreakerConfig {
433 failure_threshold: 5,
434 recovery_timeout_seconds: 30,
435 success_threshold: 2,
436 }),
437 rate_limit: None,
438 timeout: Some(TimeoutConfig {
439 duration_seconds: 60,
440 }),
441 };
442
443 let policy = config.to_policy();
444 match policy {
445 ResiliencePolicy::Combined { policies } => {
446 assert_eq!(policies.len(), 3);
447 }
448 _ => panic!("Expected combined policy"),
449 }
450 }
451
452 #[test]
453 fn test_resilience_config_from_toml() {
454 let toml_content = r#"
455 enabled = true
456
457 [policies.default]
458 retry = { max_attempts = 3, backoff_type = "exponential", initial_delay_ms = 100 }
459
460 [policies.database]
461 retry = { max_attempts = 5, backoff_type = "fixed", delay_ms = 1000 }
462 circuit_breaker = { failure_threshold = 10, recovery_timeout_seconds = 30 }
463
464 [services]
465 payment_service = "database"
466 cache_service = "default"
467 "#;
468
469 let config = ResilienceConfig::from_toml(toml_content).unwrap();
470 assert!(config.enabled);
471 assert_eq!(config.policies.len(), 2);
472 assert_eq!(config.services.len(), 2);
473
474 let payment_policy = config.get_policy_for_service("payment_service").unwrap();
476 match payment_policy {
477 ResiliencePolicy::Combined { policies } => {
478 assert_eq!(policies.len(), 2); }
480 _ => panic!("Expected combined policy for payment service"),
481 }
482 }
483
484 #[test]
485 fn test_backoff_config_conversion() {
486 let fixed_config = BackoffConfig::Fixed { delay_ms: 500 };
487 let fixed_strategy = fixed_config.to_backoff_strategy();
488 match fixed_strategy {
489 BackoffStrategy::Fixed { delay } => {
490 assert_eq!(delay, Duration::from_millis(500));
491 }
492 _ => panic!("Expected fixed backoff"),
493 }
494
495 let exp_config = BackoffConfig::Exponential {
496 initial_delay_ms: 100,
497 multiplier: 2.0,
498 max_delay_ms: Some(5000),
499 jitter: true,
500 };
501 let exp_strategy = exp_config.to_backoff_strategy();
502 match exp_strategy {
503 BackoffStrategy::Exponential {
504 initial_delay,
505 multiplier,
506 max_delay,
507 jitter,
508 } => {
509 assert_eq!(initial_delay, Duration::from_millis(100));
510 assert_eq!(multiplier, 2.0);
511 assert_eq!(max_delay, Some(Duration::from_millis(5000)));
512 assert!(jitter);
513 }
514 _ => panic!("Expected exponential backoff"),
515 }
516 }
517
518 #[test]
519 fn test_config_disabled_behavior() {
520 let config = ResilienceConfig::from_toml("enabled = false").unwrap();
521 assert!(!config.enabled);
522
523 assert_eq!(config.get_default_policy(), ResiliencePolicy::None);
525 assert_eq!(config.get_policy("any"), Some(ResiliencePolicy::None));
526 assert_eq!(
527 config.get_policy_for_service("any"),
528 Some(ResiliencePolicy::None)
529 );
530 }
531}