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