1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(default, rename_all = "snake_case")]
6pub struct ScanConfig {
7 #[serde(alias = "exclude_patterns")]
20 pub exclude_paths: Vec<String>,
21 pub max_file_size_kb: u64,
23 #[serde(default)]
28 pub exclude_submodules: bool,
29 #[serde(default)]
46 pub local_packages: Vec<String>,
47 pub auto_scan_limit: usize,
51 #[serde(default)]
65 pub additional_denylist_paths: Vec<String>,
66}
67
68impl Default for ScanConfig {
69 fn default() -> Self {
70 Self {
71 exclude_paths: Vec::new(),
72 max_file_size_kb: 512,
73 exclude_submodules: false,
74 local_packages: Vec::new(),
75 auto_scan_limit: 50_000,
76 additional_denylist_paths: Vec::new(),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(default, rename_all = "snake_case")]
84pub struct DetectionConfig {
85 pub confidence_strong: f64,
87 pub confidence_moderate: f64,
89 pub confidence_weak: f64,
91 pub max_snippet_lines: usize,
93 pub trend_rising_days: u32,
96 pub trend_stable_days: u32,
100}
101
102impl Default for DetectionConfig {
103 fn default() -> Self {
104 Self {
105 confidence_strong: 0.85,
106 confidence_moderate: 0.50,
107 confidence_weak: 0.20,
108 max_snippet_lines: 20,
109 trend_rising_days: 90,
110 trend_stable_days: 365,
111 }
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(default, rename_all = "snake_case")]
118pub struct BackupConfig {
119 pub enabled: bool,
121 pub retention_count: usize,
124 pub interval_hours: u64,
126}
127
128impl Default for BackupConfig {
129 fn default() -> Self {
130 Self {
131 enabled: true,
132 retention_count: 3,
133 interval_hours: 24,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(default, rename_all = "snake_case")]
141pub struct ServerConfig {
142 pub log_level: String,
144 pub host: String,
146 pub port: u16,
148 pub transports: Vec<String>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub call_log: Option<String>,
153}
154
155impl Default for ServerConfig {
156 fn default() -> Self {
157 Self {
158 log_level: "info".to_owned(),
159 host: "127.0.0.1".to_owned(),
160 port: 6174,
161 transports: vec!["stdio".to_owned(), "sse".to_owned(), "http".to_owned()],
162 call_log: None,
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn scan_config_defaults() {
173 let cfg = ScanConfig::default();
174 assert!(cfg.exclude_paths.is_empty());
175 assert_eq!(cfg.max_file_size_kb, 512);
176 assert_eq!(cfg.auto_scan_limit, 50_000);
177 assert!(cfg.additional_denylist_paths.is_empty());
178 }
179
180 #[test]
181 fn scan_config_deserializes_additional_denylist_paths_from_toml() {
182 let toml_src = r#"
183additional_denylist_paths = ["/mnt/nfs", "/Volumes/BackupDrive"]
184"#;
185 let cfg: ScanConfig = toml::from_str(toml_src).expect("deserialize");
186 assert_eq!(
187 cfg.additional_denylist_paths,
188 vec!["/mnt/nfs".to_owned(), "/Volumes/BackupDrive".to_owned()]
189 );
190 }
191
192 #[test]
193 fn scan_config_missing_additional_denylist_paths_defaults_to_empty() {
194 let toml_src = r#"
197exclude_paths = ["target/**"]
198max_file_size_kb = 256
199auto_scan_limit = 10_000
200"#;
201 let cfg: ScanConfig = toml::from_str(toml_src).expect("deserialize");
202 assert_eq!(cfg.exclude_paths, vec!["target/**".to_owned()]);
203 assert_eq!(cfg.max_file_size_kb, 256);
204 assert_eq!(cfg.auto_scan_limit, 10_000);
205 assert!(cfg.additional_denylist_paths.is_empty());
206 }
207
208 #[test]
209 fn detection_config_defaults() {
210 let cfg = DetectionConfig::default();
211 assert!((cfg.confidence_strong - 0.85).abs() < f64::EPSILON);
212 assert!((cfg.confidence_moderate - 0.50).abs() < f64::EPSILON);
213 assert!((cfg.confidence_weak - 0.20).abs() < f64::EPSILON);
214 assert_eq!(cfg.max_snippet_lines, 20);
215 assert_eq!(cfg.trend_rising_days, 90);
216 assert_eq!(cfg.trend_stable_days, 365);
217 }
218
219 #[test]
220 fn server_config_defaults() {
221 let cfg = ServerConfig::default();
222 assert_eq!(cfg.log_level, "info");
223 assert_eq!(cfg.host, "127.0.0.1");
224 assert_eq!(cfg.port, 6174);
225 assert_eq!(cfg.transports, vec!["stdio", "sse", "http"]);
226 assert_eq!(cfg.call_log, None);
227 }
228
229 #[test]
230 fn backup_config_defaults() {
231 let cfg = BackupConfig::default();
232 assert!(cfg.enabled);
233 assert_eq!(cfg.retention_count, 3);
234 assert_eq!(cfg.interval_hours, 24);
235 }
236
237 #[test]
238 fn config_serialization_roundtrip() {
239 let cfg = DetectionConfig::default();
240 let json = serde_json::to_string(&cfg).expect("serialize");
241 let deserialized: DetectionConfig = serde_json::from_str(&json).expect("deserialize");
242 assert!((deserialized.confidence_strong - cfg.confidence_strong).abs() < f64::EPSILON);
243 }
244}