cargo_rail/config/
change_detection.rs1use crate::error::ConfigError;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
11#[serde(rename_all = "snake_case")]
12pub enum ConfidenceProfile {
13 Strict,
15 #[default]
17 Balanced,
18 Fast,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ChangeDetectionConfig {
25 #[serde(default = "default_infrastructure_patterns")]
28 pub infrastructure: Vec<String>,
29
30 #[serde(default)]
33 pub custom: HashMap<String, Vec<String>>,
34
35 #[serde(default = "default_conservative_unclassified_owner_fallback")]
39 pub conservative_unclassified_owner_fallback: bool,
40
41 #[serde(default)]
43 pub confidence_profile: ConfidenceProfile,
44
45 #[serde(default)]
49 pub bot_pr_confidence_profile: Option<ConfidenceProfile>,
50}
51
52impl Default for ChangeDetectionConfig {
53 fn default() -> Self {
54 Self {
55 infrastructure: default_infrastructure_patterns(),
56 custom: HashMap::new(),
57 conservative_unclassified_owner_fallback: default_conservative_unclassified_owner_fallback(),
58 confidence_profile: ConfidenceProfile::default(),
59 bot_pr_confidence_profile: None,
60 }
61 }
62}
63
64impl ChangeDetectionConfig {
65 pub fn validate(&self) -> Result<(), ConfigError> {
67 for pattern in &self.infrastructure {
69 if let Err(e) = glob::Pattern::new(pattern) {
70 return Err(ConfigError::InvalidGlobPattern {
71 pattern: pattern.clone(),
72 message: e.to_string(),
73 });
74 }
75 }
76
77 for (category, patterns) in &self.custom {
79 if !is_valid_custom_category(category) {
80 return Err(ConfigError::InvalidValue {
81 field: format!("change-detection.custom.{}", category),
82 message: "invalid category name; use ASCII letters, digits, '_' or '-' (cannot start with 'custom:')"
83 .to_string(),
84 });
85 }
86
87 for pattern in patterns {
88 if let Err(e) = glob::Pattern::new(pattern) {
89 return Err(ConfigError::InvalidGlobPattern {
90 pattern: format!("{}: {}", category, pattern),
91 message: e.to_string(),
92 });
93 }
94 }
95 }
96
97 Ok(())
98 }
99}
100
101fn default_infrastructure_patterns() -> Vec<String> {
102 vec![
103 ".github/**".to_string(),
104 "scripts/**".to_string(),
105 "justfile".to_string(),
106 "Justfile".to_string(),
107 "Makefile".to_string(),
108 "makefile".to_string(),
109 "GNUmakefile".to_string(),
110 "*.sh".to_string(),
111 "Taskfile.yml".to_string(),
112 "Taskfile.yaml".to_string(),
113 ".pre-commit-config.yaml".to_string(),
114 "deny.toml".to_string(),
115 "cliff.toml".to_string(),
116 "release.toml".to_string(),
117 "release-plz.toml".to_string(),
118 ]
119}
120
121fn default_conservative_unclassified_owner_fallback() -> bool {
122 true
123}
124
125fn is_valid_custom_category(category: &str) -> bool {
126 if category.is_empty() || category.starts_with("custom:") {
127 return false;
128 }
129
130 category
131 .bytes()
132 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
133}
134
135#[cfg(test)]
136mod tests {
137 use super::{ChangeDetectionConfig, ConfidenceProfile};
138 use std::collections::HashMap;
139
140 #[test]
141 fn test_validate_accepts_valid_custom_category_names() {
142 let mut custom = HashMap::new();
143 custom.insert("verify_models".to_string(), vec!["verify/**".to_string()]);
144 custom.insert("bench-extended".to_string(), vec!["perf/**".to_string()]);
145 let cfg = ChangeDetectionConfig {
146 infrastructure: vec![".github/**".to_string()],
147 custom,
148 conservative_unclassified_owner_fallback: true,
149 confidence_profile: ConfidenceProfile::Balanced,
150 bot_pr_confidence_profile: None,
151 };
152 assert!(cfg.validate().is_ok());
153 }
154
155 #[test]
156 fn test_validate_rejects_invalid_custom_category_names() {
157 let mut custom = HashMap::new();
158 custom.insert("custom:verify".to_string(), vec!["verify/**".to_string()]);
159 let cfg = ChangeDetectionConfig {
160 infrastructure: vec![".github/**".to_string()],
161 custom,
162 conservative_unclassified_owner_fallback: true,
163 confidence_profile: ConfidenceProfile::Balanced,
164 bot_pr_confidence_profile: None,
165 };
166 assert!(cfg.validate().is_err());
167 }
168}