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 {
56 7
57}
58fn default_weekly() -> usize {
59 4
60}
61fn default_monthly() -> usize {
62 12
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ThresholdConfig {
70 #[serde(default = "default_fan_in_warning")]
72 pub fan_in_warning: usize,
73 #[serde(default = "default_fan_in_critical")]
75 pub fan_in_critical: usize,
76 #[serde(default = "default_cycle_length")]
78 pub cycle_length: usize,
79 #[serde(default = "default_module_file_count")]
81 pub module_file_count: usize,
82 #[serde(default = "default_line_count_growth")]
84 pub line_count_growth: f64,
85}
86
87impl Default for ThresholdConfig {
88 fn default() -> Self {
89 Self {
90 fan_in_warning: default_fan_in_warning(),
91 fan_in_critical: default_fan_in_critical(),
92 cycle_length: default_cycle_length(),
93 module_file_count: default_module_file_count(),
94 line_count_growth: default_line_count_growth(),
95 }
96 }
97}
98
99fn default_fan_in_warning() -> usize {
100 10
101}
102fn default_fan_in_critical() -> usize {
103 25
104}
105fn default_cycle_length() -> usize {
106 3
107}
108fn default_module_file_count() -> usize {
109 50
110}
111fn default_line_count_growth() -> f64 {
112 2.0
113}
114
115pub fn load_pulse_config(cache_path: &Path) -> Result<PulseConfig> {
119 let config_path = cache_path.join("config.toml");
120
121 if !config_path.exists() {
122 return Ok(PulseConfig::default());
123 }
124
125 let content = std::fs::read_to_string(&config_path)?;
126 let table: toml::Value = content.parse()?;
127
128 if let Some(pulse_section) = table.get("pulse") {
129 let config: PulseConfig = pulse_section.clone().try_into()?;
130 Ok(config)
131 } else {
132 Ok(PulseConfig::default())
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_default_config() {
142 let config = PulseConfig::default();
143 assert_eq!(config.retention.daily, 7);
144 assert_eq!(config.retention.weekly, 4);
145 assert_eq!(config.retention.monthly, 12);
146 assert_eq!(config.thresholds.fan_in_warning, 10);
147 assert_eq!(config.thresholds.fan_in_critical, 25);
148 assert_eq!(config.thresholds.cycle_length, 3);
149 assert_eq!(config.thresholds.module_file_count, 50);
150 assert!((config.thresholds.line_count_growth - 2.0).abs() < f64::EPSILON);
151 }
152
153 #[test]
154 fn test_load_missing_config() {
155 let config = load_pulse_config(Path::new("/nonexistent")).unwrap();
156 assert_eq!(config.retention.daily, 7);
157 }
158
159 #[test]
160 fn test_deserialize_partial_config() {
161 let toml_str = r#"
162 [pulse.retention]
163 daily = 14
164 "#;
165 let table: toml::Value = toml_str.parse().unwrap();
166 let pulse_section = table.get("pulse").unwrap();
167 let config: PulseConfig = pulse_section.clone().try_into().unwrap();
168 assert_eq!(config.retention.daily, 14);
169 assert_eq!(config.retention.weekly, 4); assert_eq!(config.thresholds.fan_in_warning, 10); }
172}