use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
const fn default_max_cyclomatic() -> u16 {
20
}
const fn default_max_cognitive() -> u16 {
15
}
fn default_bot_patterns() -> Vec<String> {
vec![
r"*\[bot\]*".to_string(),
"dependabot*".to_string(),
"renovate*".to_string(),
"github-actions*".to_string(),
"svc-*".to_string(),
"*-service-account*".to_string(),
]
}
const fn default_email_mode() -> EmailMode {
EmailMode::Handle
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum EmailMode {
Raw,
Handle,
Hash,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct OwnershipConfig {
#[serde(default = "default_bot_patterns")]
pub bot_patterns: Vec<String>,
#[serde(default = "default_email_mode")]
pub email_mode: EmailMode,
}
impl Default for OwnershipConfig {
fn default() -> Self {
Self {
bot_patterns: default_bot_patterns(),
email_mode: default_email_mode(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct HealthConfig {
#[serde(default = "default_max_cyclomatic")]
pub max_cyclomatic: u16,
#[serde(default = "default_max_cognitive")]
pub max_cognitive: u16,
#[serde(default)]
pub ignore: Vec<String>,
#[serde(default)]
pub ownership: OwnershipConfig,
}
impl Default for HealthConfig {
fn default() -> Self {
Self {
max_cyclomatic: default_max_cyclomatic(),
max_cognitive: default_max_cognitive(),
ignore: vec![],
ownership: OwnershipConfig::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn health_config_defaults() {
let config = HealthConfig::default();
assert_eq!(config.max_cyclomatic, 20);
assert_eq!(config.max_cognitive, 15);
assert!(config.ignore.is_empty());
}
#[test]
fn health_config_json_all_fields() {
let json = r#"{
"maxCyclomatic": 30,
"maxCognitive": 25,
"ignore": ["**/generated/**", "vendor/**"]
}"#;
let config: HealthConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.max_cyclomatic, 30);
assert_eq!(config.max_cognitive, 25);
assert_eq!(config.ignore, vec!["**/generated/**", "vendor/**"]);
}
#[test]
fn health_config_json_partial_uses_defaults() {
let json = r#"{"maxCyclomatic": 10}"#;
let config: HealthConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.max_cyclomatic, 10);
assert_eq!(config.max_cognitive, 15); assert!(config.ignore.is_empty()); }
#[test]
fn health_config_json_empty_object_uses_all_defaults() {
let config: HealthConfig = serde_json::from_str("{}").unwrap();
assert_eq!(config.max_cyclomatic, 20);
assert_eq!(config.max_cognitive, 15);
assert!(config.ignore.is_empty());
}
#[test]
fn health_config_json_only_ignore() {
let json = r#"{"ignore": ["test/**"]}"#;
let config: HealthConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.max_cyclomatic, 20); assert_eq!(config.max_cognitive, 15); assert_eq!(config.ignore, vec!["test/**"]);
}
#[test]
fn health_config_toml_all_fields() {
let toml_str = r#"
maxCyclomatic = 25
maxCognitive = 20
ignore = ["generated/**", "vendor/**"]
"#;
let config: HealthConfig = toml::from_str(toml_str).unwrap();
assert_eq!(config.max_cyclomatic, 25);
assert_eq!(config.max_cognitive, 20);
assert_eq!(config.ignore, vec!["generated/**", "vendor/**"]);
}
#[test]
fn health_config_toml_defaults() {
let config: HealthConfig = toml::from_str("").unwrap();
assert_eq!(config.max_cyclomatic, 20);
assert_eq!(config.max_cognitive, 15);
assert!(config.ignore.is_empty());
}
#[test]
fn health_config_json_roundtrip() {
let config = HealthConfig {
max_cyclomatic: 50,
max_cognitive: 40,
ignore: vec!["test/**".to_string()],
ownership: OwnershipConfig::default(),
};
let json = serde_json::to_string(&config).unwrap();
let restored: HealthConfig = serde_json::from_str(&json).unwrap();
assert_eq!(restored.max_cyclomatic, 50);
assert_eq!(restored.max_cognitive, 40);
assert_eq!(restored.ignore, vec!["test/**"]);
}
#[test]
fn health_config_zero_thresholds() {
let json = r#"{"maxCyclomatic": 0, "maxCognitive": 0}"#;
let config: HealthConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.max_cyclomatic, 0);
assert_eq!(config.max_cognitive, 0);
}
#[test]
fn health_config_large_thresholds() {
let json = r#"{"maxCyclomatic": 65535, "maxCognitive": 65535}"#;
let config: HealthConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.max_cyclomatic, u16::MAX);
assert_eq!(config.max_cognitive, u16::MAX);
}
#[test]
fn ownership_config_default_has_bot_patterns() {
let cfg = OwnershipConfig::default();
assert!(cfg.bot_patterns.iter().any(|p| p == r"*\[bot\]*"));
assert!(cfg.bot_patterns.iter().any(|p| p == "dependabot*"));
assert!(cfg.bot_patterns.iter().any(|p| p == "github-actions*"));
assert!(
!cfg.bot_patterns.iter().any(|p| p == "*noreply*"),
"*noreply* must not be a default bot pattern (filters real human \
contributors using GitHub's privacy default email)"
);
assert_eq!(cfg.email_mode, EmailMode::Handle);
}
#[test]
fn ownership_config_default_via_health() {
let cfg = HealthConfig::default();
assert_eq!(cfg.ownership.email_mode, EmailMode::Handle);
assert!(!cfg.ownership.bot_patterns.is_empty());
}
#[test]
fn ownership_config_json_overrides_defaults() {
let json = r#"{
"ownership": {
"botPatterns": ["custom-bot*"],
"emailMode": "raw"
}
}"#;
let config: HealthConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.ownership.bot_patterns, vec!["custom-bot*"]);
assert_eq!(config.ownership.email_mode, EmailMode::Raw);
}
#[test]
fn ownership_config_email_mode_kebab_case() {
for (mode, repr) in [
(EmailMode::Raw, "\"raw\""),
(EmailMode::Handle, "\"handle\""),
(EmailMode::Hash, "\"hash\""),
] {
let s = serde_json::to_string(&mode).unwrap();
assert_eq!(s, repr);
let back: EmailMode = serde_json::from_str(repr).unwrap();
assert_eq!(back, mode);
}
}
}