1use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::path::Path;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PulseConfig {
13 #[serde(default)]
14 pub retention: RetentionConfig,
15 #[serde(default)]
16 pub thresholds: ThresholdConfig,
17}
18
19impl Default for PulseConfig {
20 fn default() -> Self {
21 Self {
22 retention: RetentionConfig::default(),
23 thresholds: ThresholdConfig::default(),
24 }
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RetentionConfig {
34 #[serde(default = "default_daily")]
36 pub daily: usize,
37 #[serde(default = "default_weekly")]
39 pub weekly: usize,
40 #[serde(default = "default_monthly")]
42 pub monthly: usize,
43}
44
45impl Default for RetentionConfig {
46 fn default() -> Self {
47 Self {
48 daily: default_daily(),
49 weekly: default_weekly(),
50 monthly: default_monthly(),
51 }
52 }
53}
54
55fn default_daily() -> usize { 7 }
56fn default_weekly() -> usize { 4 }
57fn default_monthly() -> usize { 12 }
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ThresholdConfig {
64 #[serde(default = "default_fan_in_warning")]
66 pub fan_in_warning: usize,
67 #[serde(default = "default_fan_in_critical")]
69 pub fan_in_critical: usize,
70 #[serde(default = "default_cycle_length")]
72 pub cycle_length: usize,
73 #[serde(default = "default_module_file_count")]
75 pub module_file_count: usize,
76 #[serde(default = "default_line_count_growth")]
78 pub line_count_growth: f64,
79}
80
81impl Default for ThresholdConfig {
82 fn default() -> Self {
83 Self {
84 fan_in_warning: default_fan_in_warning(),
85 fan_in_critical: default_fan_in_critical(),
86 cycle_length: default_cycle_length(),
87 module_file_count: default_module_file_count(),
88 line_count_growth: default_line_count_growth(),
89 }
90 }
91}
92
93fn default_fan_in_warning() -> usize { 10 }
94fn default_fan_in_critical() -> usize { 25 }
95fn default_cycle_length() -> usize { 3 }
96fn default_module_file_count() -> usize { 50 }
97fn default_line_count_growth() -> f64 { 2.0 }
98
99pub fn load_pulse_config(cache_path: &Path) -> Result<PulseConfig> {
103 let config_path = cache_path.join("config.toml");
104
105 if !config_path.exists() {
106 return Ok(PulseConfig::default());
107 }
108
109 let content = std::fs::read_to_string(&config_path)?;
110 let table: toml::Value = content.parse()?;
111
112 if let Some(pulse_section) = table.get("pulse") {
113 let config: PulseConfig = pulse_section.clone().try_into()?;
114 Ok(config)
115 } else {
116 Ok(PulseConfig::default())
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_default_config() {
126 let config = PulseConfig::default();
127 assert_eq!(config.retention.daily, 7);
128 assert_eq!(config.retention.weekly, 4);
129 assert_eq!(config.retention.monthly, 12);
130 assert_eq!(config.thresholds.fan_in_warning, 10);
131 assert_eq!(config.thresholds.fan_in_critical, 25);
132 assert_eq!(config.thresholds.cycle_length, 3);
133 assert_eq!(config.thresholds.module_file_count, 50);
134 assert!((config.thresholds.line_count_growth - 2.0).abs() < f64::EPSILON);
135 }
136
137 #[test]
138 fn test_load_missing_config() {
139 let config = load_pulse_config(Path::new("/nonexistent")).unwrap();
140 assert_eq!(config.retention.daily, 7);
141 }
142
143 #[test]
144 fn test_deserialize_partial_config() {
145 let toml_str = r#"
146 [pulse.retention]
147 daily = 14
148 "#;
149 let table: toml::Value = toml_str.parse().unwrap();
150 let pulse_section = table.get("pulse").unwrap();
151 let config: PulseConfig = pulse_section.clone().try_into().unwrap();
152 assert_eq!(config.retention.daily, 14);
153 assert_eq!(config.retention.weekly, 4); assert_eq!(config.thresholds.fan_in_warning, 10); }
156}