1use serde::{Deserialize, Serialize};
7
8use crate::error::Result;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct GateConfig {
13 pub max_complexity: u32,
15 pub satd_tolerance: u32,
17 pub dead_code_max_percent: f64,
19 pub min_quality_score: f64,
21}
22
23impl Default for GateConfig {
24 fn default() -> Self {
25 Self {
26 max_complexity: 20,
27 satd_tolerance: 0,
28 dead_code_max_percent: 10.0,
29 min_quality_score: 80.0,
30 }
31 }
32}
33
34pub struct QualityGate {
36 config: GateConfig,
37}
38
39impl QualityGate {
40 #[must_use]
42 pub const fn new(config: GateConfig) -> Self {
43 Self { config }
44 }
45
46 pub fn analyze(&self, analysis: &QualityAnalysis) -> Result<GateResult> {
51 let mut violations = Vec::new();
52
53 if analysis.max_complexity > self.config.max_complexity {
55 violations.push(QualityViolation::Complexity {
56 actual: analysis.max_complexity,
57 threshold: self.config.max_complexity,
58 location: analysis.complexity_hotspot.clone(),
59 });
60 }
61
62 if analysis.satd_count > self.config.satd_tolerance {
64 violations.push(QualityViolation::TechnicalDebt {
65 count: analysis.satd_count,
66 tolerance: self.config.satd_tolerance,
67 });
68 }
69
70 if analysis.dead_code_percent > self.config.dead_code_max_percent {
72 violations.push(QualityViolation::DeadCode {
73 percent: analysis.dead_code_percent,
74 threshold: self.config.dead_code_max_percent,
75 });
76 }
77
78 if analysis.quality_score < self.config.min_quality_score {
80 violations.push(QualityViolation::QualityScore {
81 score: analysis.quality_score,
82 minimum: self.config.min_quality_score,
83 });
84 }
85
86 if violations.is_empty() {
87 Ok(GateResult::Passed)
88 } else {
89 Ok(GateResult::Failed { violations })
90 }
91 }
92
93 #[must_use]
95 pub const fn config(&self) -> &GateConfig {
96 &self.config
97 }
98}
99
100impl Default for QualityGate {
101 fn default() -> Self {
102 Self::new(GateConfig::default())
103 }
104}
105
106#[derive(Debug, Clone)]
108pub enum GateResult {
109 Passed,
111 Failed {
113 violations: Vec<QualityViolation>,
115 },
116}
117
118impl GateResult {
119 #[must_use]
121 pub const fn passed(&self) -> bool {
122 matches!(self, Self::Passed)
123 }
124}
125
126#[derive(Debug, Clone)]
128pub enum QualityViolation {
129 Complexity {
131 actual: u32,
133 threshold: u32,
135 location: Option<String>,
137 },
138 TechnicalDebt {
140 count: u32,
142 tolerance: u32,
144 },
145 DeadCode {
147 percent: f64,
149 threshold: f64,
151 },
152 QualityScore {
154 score: f64,
156 minimum: f64,
158 },
159}
160
161#[derive(Debug, Clone, Default)]
163pub struct QualityAnalysis {
164 pub max_complexity: u32,
166 pub complexity_hotspot: Option<String>,
168 pub satd_count: u32,
170 pub dead_code_percent: f64,
172 pub quality_score: f64,
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_gate_passes_good_code() {
182 let gate = QualityGate::default();
183 let analysis = QualityAnalysis {
184 max_complexity: 10,
185 satd_count: 0,
186 dead_code_percent: 5.0,
187 quality_score: 90.0,
188 ..Default::default()
189 };
190
191 let result = gate.analyze(&analysis).ok();
192 assert!(result.is_some_and(|r| r.passed()));
193 }
194
195 #[test]
196 fn test_gate_fails_on_complexity() {
197 let gate = QualityGate::new(GateConfig {
198 max_complexity: 10,
199 ..Default::default()
200 });
201
202 let analysis = QualityAnalysis {
203 max_complexity: 25,
204 ..Default::default()
205 };
206
207 let result = gate.analyze(&analysis).ok();
208 assert!(result.is_some_and(|r| !r.passed()));
209 }
210
211 #[test]
212 fn test_gate_fails_on_satd() {
213 let gate = QualityGate::new(GateConfig {
214 satd_tolerance: 0,
215 ..Default::default()
216 });
217
218 let analysis = QualityAnalysis {
219 satd_count: 5,
220 quality_score: 90.0,
221 ..Default::default()
222 };
223
224 let result = gate.analyze(&analysis).ok();
225 assert!(result.is_some_and(|r| !r.passed()));
226 }
227}