1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4const fn default_max_cyclomatic() -> u16 {
5 20
6}
7
8const fn default_max_cognitive() -> u16 {
9 15
10}
11
12const fn default_max_crap() -> f64 {
16 30.0
17}
18
19const fn default_suggest_inline_suppression() -> bool {
23 true
24}
25
26fn default_bot_patterns() -> Vec<String> {
41 vec![
42 r"*\[bot\]*".to_string(),
43 "dependabot*".to_string(),
44 "renovate*".to_string(),
45 "github-actions*".to_string(),
46 "svc-*".to_string(),
47 "*-service-account*".to_string(),
48 ]
49}
50
51const fn default_email_mode() -> EmailMode {
52 EmailMode::Handle
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
60#[serde(rename_all = "kebab-case")]
61pub enum EmailMode {
62 Raw,
65 Handle,
68 Hash,
74}
75
76#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
78#[serde(rename_all = "camelCase")]
79pub struct OwnershipConfig {
80 #[serde(default = "default_bot_patterns")]
84 pub bot_patterns: Vec<String>,
85
86 #[serde(default = "default_email_mode")]
89 pub email_mode: EmailMode,
90}
91
92impl Default for OwnershipConfig {
93 fn default() -> Self {
94 Self {
95 bot_patterns: default_bot_patterns(),
96 email_mode: default_email_mode(),
97 }
98 }
99}
100
101#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
103#[serde(rename_all = "camelCase")]
104pub struct HealthConfig {
105 #[serde(default = "default_max_cyclomatic")]
108 pub max_cyclomatic: u16,
109
110 #[serde(default = "default_max_cognitive")]
113 pub max_cognitive: u16,
114
115 #[serde(default = "default_max_crap")]
122 pub max_crap: f64,
123
124 #[serde(default)]
126 pub ignore: Vec<String>,
127
128 #[serde(default)]
131 pub ownership: OwnershipConfig,
132
133 #[serde(default = "default_suggest_inline_suppression")]
142 pub suggest_inline_suppression: bool,
143}
144
145impl Default for HealthConfig {
146 fn default() -> Self {
147 Self {
148 max_cyclomatic: default_max_cyclomatic(),
149 max_cognitive: default_max_cognitive(),
150 max_crap: default_max_crap(),
151 ignore: vec![],
152 ownership: OwnershipConfig::default(),
153 suggest_inline_suppression: default_suggest_inline_suppression(),
154 }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn health_config_defaults() {
164 let config = HealthConfig::default();
165 assert_eq!(config.max_cyclomatic, 20);
166 assert_eq!(config.max_cognitive, 15);
167 assert!((config.max_crap - 30.0).abs() < f64::EPSILON);
168 assert!(config.ignore.is_empty());
169 }
170
171 #[test]
172 fn health_config_json_all_fields() {
173 let json = r#"{
174 "maxCyclomatic": 30,
175 "maxCognitive": 25,
176 "maxCrap": 50.0,
177 "ignore": ["**/generated/**", "vendor/**"]
178 }"#;
179 let config: HealthConfig = serde_json::from_str(json).unwrap();
180 assert_eq!(config.max_cyclomatic, 30);
181 assert_eq!(config.max_cognitive, 25);
182 assert!((config.max_crap - 50.0).abs() < f64::EPSILON);
183 assert_eq!(config.ignore, vec!["**/generated/**", "vendor/**"]);
184 }
185
186 #[test]
187 fn health_config_json_partial_uses_defaults() {
188 let json = r#"{"maxCyclomatic": 10}"#;
189 let config: HealthConfig = serde_json::from_str(json).unwrap();
190 assert_eq!(config.max_cyclomatic, 10);
191 assert_eq!(config.max_cognitive, 15); assert!((config.max_crap - 30.0).abs() < f64::EPSILON); assert!(config.ignore.is_empty()); }
195
196 #[test]
197 fn health_config_json_only_max_crap() {
198 let json = r#"{"maxCrap": 15.5}"#;
199 let config: HealthConfig = serde_json::from_str(json).unwrap();
200 assert!((config.max_crap - 15.5).abs() < f64::EPSILON);
201 assert_eq!(config.max_cyclomatic, 20); assert_eq!(config.max_cognitive, 15); }
204
205 #[test]
206 fn health_config_json_empty_object_uses_all_defaults() {
207 let config: HealthConfig = serde_json::from_str("{}").unwrap();
208 assert_eq!(config.max_cyclomatic, 20);
209 assert_eq!(config.max_cognitive, 15);
210 assert!(config.ignore.is_empty());
211 }
212
213 #[test]
214 fn health_config_json_only_ignore() {
215 let json = r#"{"ignore": ["test/**"]}"#;
216 let config: HealthConfig = serde_json::from_str(json).unwrap();
217 assert_eq!(config.max_cyclomatic, 20); assert_eq!(config.max_cognitive, 15); assert_eq!(config.ignore, vec!["test/**"]);
220 }
221
222 #[test]
225 fn health_config_toml_all_fields() {
226 let toml_str = r#"
227maxCyclomatic = 25
228maxCognitive = 20
229ignore = ["generated/**", "vendor/**"]
230"#;
231 let config: HealthConfig = toml::from_str(toml_str).unwrap();
232 assert_eq!(config.max_cyclomatic, 25);
233 assert_eq!(config.max_cognitive, 20);
234 assert_eq!(config.ignore, vec!["generated/**", "vendor/**"]);
235 }
236
237 #[test]
238 fn health_config_toml_defaults() {
239 let config: HealthConfig = toml::from_str("").unwrap();
240 assert_eq!(config.max_cyclomatic, 20);
241 assert_eq!(config.max_cognitive, 15);
242 assert!(config.ignore.is_empty());
243 }
244
245 #[test]
248 fn health_config_json_roundtrip() {
249 let config = HealthConfig {
250 max_cyclomatic: 50,
251 max_cognitive: 40,
252 max_crap: 75.0,
253 ignore: vec!["test/**".to_string()],
254 ownership: OwnershipConfig::default(),
255 suggest_inline_suppression: false,
256 };
257 let json = serde_json::to_string(&config).unwrap();
258 let restored: HealthConfig = serde_json::from_str(&json).unwrap();
259 assert_eq!(restored.max_cyclomatic, 50);
260 assert_eq!(restored.max_cognitive, 40);
261 assert!((restored.max_crap - 75.0).abs() < f64::EPSILON);
262 assert_eq!(restored.ignore, vec!["test/**"]);
263 assert!(!restored.suggest_inline_suppression);
264 }
265
266 #[test]
267 fn health_config_suggest_inline_suppression_default_true() {
268 let config = HealthConfig::default();
269 assert!(config.suggest_inline_suppression);
270 }
271
272 #[test]
273 fn health_config_suggest_inline_suppression_explicit_false() {
274 let json = r#"{"suggestInlineSuppression": false}"#;
275 let config: HealthConfig = serde_json::from_str(json).unwrap();
276 assert!(!config.suggest_inline_suppression);
277 }
278
279 #[test]
280 fn health_config_suggest_inline_suppression_omitted_uses_default() {
281 let config: HealthConfig = serde_json::from_str("{}").unwrap();
282 assert!(config.suggest_inline_suppression);
283 }
284
285 #[test]
288 fn health_config_zero_thresholds() {
289 let json = r#"{"maxCyclomatic": 0, "maxCognitive": 0}"#;
290 let config: HealthConfig = serde_json::from_str(json).unwrap();
291 assert_eq!(config.max_cyclomatic, 0);
292 assert_eq!(config.max_cognitive, 0);
293 }
294
295 #[test]
298 fn health_config_large_thresholds() {
299 let json = r#"{"maxCyclomatic": 65535, "maxCognitive": 65535}"#;
300 let config: HealthConfig = serde_json::from_str(json).unwrap();
301 assert_eq!(config.max_cyclomatic, u16::MAX);
302 assert_eq!(config.max_cognitive, u16::MAX);
303 }
304
305 #[test]
308 fn ownership_config_default_has_bot_patterns() {
309 let cfg = OwnershipConfig::default();
310 assert!(cfg.bot_patterns.iter().any(|p| p == r"*\[bot\]*"));
313 assert!(cfg.bot_patterns.iter().any(|p| p == "dependabot*"));
314 assert!(cfg.bot_patterns.iter().any(|p| p == "github-actions*"));
315 assert!(
319 !cfg.bot_patterns.iter().any(|p| p == "*noreply*"),
320 "*noreply* must not be a default bot pattern (filters real human \
321 contributors using GitHub's privacy default email)"
322 );
323 assert_eq!(cfg.email_mode, EmailMode::Handle);
324 }
325
326 #[test]
327 fn ownership_config_default_via_health() {
328 let cfg = HealthConfig::default();
329 assert_eq!(cfg.ownership.email_mode, EmailMode::Handle);
330 assert!(!cfg.ownership.bot_patterns.is_empty());
331 }
332
333 #[test]
334 fn ownership_config_json_overrides_defaults() {
335 let json = r#"{
336 "ownership": {
337 "botPatterns": ["custom-bot*"],
338 "emailMode": "raw"
339 }
340 }"#;
341 let config: HealthConfig = serde_json::from_str(json).unwrap();
342 assert_eq!(config.ownership.bot_patterns, vec!["custom-bot*"]);
343 assert_eq!(config.ownership.email_mode, EmailMode::Raw);
344 }
345
346 #[test]
347 fn ownership_config_email_mode_kebab_case() {
348 for (mode, repr) in [
350 (EmailMode::Raw, "\"raw\""),
351 (EmailMode::Handle, "\"handle\""),
352 (EmailMode::Hash, "\"hash\""),
353 ] {
354 let s = serde_json::to_string(&mode).unwrap();
355 assert_eq!(s, repr);
356 let back: EmailMode = serde_json::from_str(repr).unwrap();
357 assert_eq!(back, mode);
358 }
359 }
360}