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
12fn default_bot_patterns() -> Vec<String> {
27 vec![
28 r"*\[bot\]*".to_string(),
29 "dependabot*".to_string(),
30 "renovate*".to_string(),
31 "github-actions*".to_string(),
32 "svc-*".to_string(),
33 "*-service-account*".to_string(),
34 ]
35}
36
37const fn default_email_mode() -> EmailMode {
38 EmailMode::Handle
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
46#[serde(rename_all = "kebab-case")]
47pub enum EmailMode {
48 Raw,
51 Handle,
54 Hash,
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
64#[serde(rename_all = "camelCase")]
65pub struct OwnershipConfig {
66 #[serde(default = "default_bot_patterns")]
70 pub bot_patterns: Vec<String>,
71
72 #[serde(default = "default_email_mode")]
75 pub email_mode: EmailMode,
76}
77
78impl Default for OwnershipConfig {
79 fn default() -> Self {
80 Self {
81 bot_patterns: default_bot_patterns(),
82 email_mode: default_email_mode(),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
89#[serde(rename_all = "camelCase")]
90pub struct HealthConfig {
91 #[serde(default = "default_max_cyclomatic")]
94 pub max_cyclomatic: u16,
95
96 #[serde(default = "default_max_cognitive")]
99 pub max_cognitive: u16,
100
101 #[serde(default)]
103 pub ignore: Vec<String>,
104
105 #[serde(default)]
108 pub ownership: OwnershipConfig,
109}
110
111impl Default for HealthConfig {
112 fn default() -> Self {
113 Self {
114 max_cyclomatic: default_max_cyclomatic(),
115 max_cognitive: default_max_cognitive(),
116 ignore: vec![],
117 ownership: OwnershipConfig::default(),
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn health_config_defaults() {
128 let config = HealthConfig::default();
129 assert_eq!(config.max_cyclomatic, 20);
130 assert_eq!(config.max_cognitive, 15);
131 assert!(config.ignore.is_empty());
132 }
133
134 #[test]
135 fn health_config_json_all_fields() {
136 let json = r#"{
137 "maxCyclomatic": 30,
138 "maxCognitive": 25,
139 "ignore": ["**/generated/**", "vendor/**"]
140 }"#;
141 let config: HealthConfig = serde_json::from_str(json).unwrap();
142 assert_eq!(config.max_cyclomatic, 30);
143 assert_eq!(config.max_cognitive, 25);
144 assert_eq!(config.ignore, vec!["**/generated/**", "vendor/**"]);
145 }
146
147 #[test]
148 fn health_config_json_partial_uses_defaults() {
149 let json = r#"{"maxCyclomatic": 10}"#;
150 let config: HealthConfig = serde_json::from_str(json).unwrap();
151 assert_eq!(config.max_cyclomatic, 10);
152 assert_eq!(config.max_cognitive, 15); assert!(config.ignore.is_empty()); }
155
156 #[test]
157 fn health_config_json_empty_object_uses_all_defaults() {
158 let config: HealthConfig = serde_json::from_str("{}").unwrap();
159 assert_eq!(config.max_cyclomatic, 20);
160 assert_eq!(config.max_cognitive, 15);
161 assert!(config.ignore.is_empty());
162 }
163
164 #[test]
165 fn health_config_json_only_ignore() {
166 let json = r#"{"ignore": ["test/**"]}"#;
167 let config: HealthConfig = serde_json::from_str(json).unwrap();
168 assert_eq!(config.max_cyclomatic, 20); assert_eq!(config.max_cognitive, 15); assert_eq!(config.ignore, vec!["test/**"]);
171 }
172
173 #[test]
176 fn health_config_toml_all_fields() {
177 let toml_str = r#"
178maxCyclomatic = 25
179maxCognitive = 20
180ignore = ["generated/**", "vendor/**"]
181"#;
182 let config: HealthConfig = toml::from_str(toml_str).unwrap();
183 assert_eq!(config.max_cyclomatic, 25);
184 assert_eq!(config.max_cognitive, 20);
185 assert_eq!(config.ignore, vec!["generated/**", "vendor/**"]);
186 }
187
188 #[test]
189 fn health_config_toml_defaults() {
190 let config: HealthConfig = toml::from_str("").unwrap();
191 assert_eq!(config.max_cyclomatic, 20);
192 assert_eq!(config.max_cognitive, 15);
193 assert!(config.ignore.is_empty());
194 }
195
196 #[test]
199 fn health_config_json_roundtrip() {
200 let config = HealthConfig {
201 max_cyclomatic: 50,
202 max_cognitive: 40,
203 ignore: vec!["test/**".to_string()],
204 ownership: OwnershipConfig::default(),
205 };
206 let json = serde_json::to_string(&config).unwrap();
207 let restored: HealthConfig = serde_json::from_str(&json).unwrap();
208 assert_eq!(restored.max_cyclomatic, 50);
209 assert_eq!(restored.max_cognitive, 40);
210 assert_eq!(restored.ignore, vec!["test/**"]);
211 }
212
213 #[test]
216 fn health_config_zero_thresholds() {
217 let json = r#"{"maxCyclomatic": 0, "maxCognitive": 0}"#;
218 let config: HealthConfig = serde_json::from_str(json).unwrap();
219 assert_eq!(config.max_cyclomatic, 0);
220 assert_eq!(config.max_cognitive, 0);
221 }
222
223 #[test]
226 fn health_config_large_thresholds() {
227 let json = r#"{"maxCyclomatic": 65535, "maxCognitive": 65535}"#;
228 let config: HealthConfig = serde_json::from_str(json).unwrap();
229 assert_eq!(config.max_cyclomatic, u16::MAX);
230 assert_eq!(config.max_cognitive, u16::MAX);
231 }
232
233 #[test]
236 fn ownership_config_default_has_bot_patterns() {
237 let cfg = OwnershipConfig::default();
238 assert!(cfg.bot_patterns.iter().any(|p| p == r"*\[bot\]*"));
241 assert!(cfg.bot_patterns.iter().any(|p| p == "dependabot*"));
242 assert!(cfg.bot_patterns.iter().any(|p| p == "github-actions*"));
243 assert!(
247 !cfg.bot_patterns.iter().any(|p| p == "*noreply*"),
248 "*noreply* must not be a default bot pattern (filters real human \
249 contributors using GitHub's privacy default email)"
250 );
251 assert_eq!(cfg.email_mode, EmailMode::Handle);
252 }
253
254 #[test]
255 fn ownership_config_default_via_health() {
256 let cfg = HealthConfig::default();
257 assert_eq!(cfg.ownership.email_mode, EmailMode::Handle);
258 assert!(!cfg.ownership.bot_patterns.is_empty());
259 }
260
261 #[test]
262 fn ownership_config_json_overrides_defaults() {
263 let json = r#"{
264 "ownership": {
265 "botPatterns": ["custom-bot*"],
266 "emailMode": "raw"
267 }
268 }"#;
269 let config: HealthConfig = serde_json::from_str(json).unwrap();
270 assert_eq!(config.ownership.bot_patterns, vec!["custom-bot*"]);
271 assert_eq!(config.ownership.email_mode, EmailMode::Raw);
272 }
273
274 #[test]
275 fn ownership_config_email_mode_kebab_case() {
276 for (mode, repr) in [
278 (EmailMode::Raw, "\"raw\""),
279 (EmailMode::Handle, "\"handle\""),
280 (EmailMode::Hash, "\"hash\""),
281 ] {
282 let s = serde_json::to_string(&mode).unwrap();
283 assert_eq!(s, repr);
284 let back: EmailMode = serde_json::from_str(repr).unwrap();
285 assert_eq!(back, mode);
286 }
287 }
288}