1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct CheckReport {
10 pub commits: Vec<CommitCheckResult>,
12 pub summary: CheckSummary,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct CommitCheckResult {
19 pub hash: String,
21 pub message: String,
23 pub issues: Vec<CommitIssue>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub suggestion: Option<CommitSuggestion>,
28 pub passes: bool,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub summary: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CommitIssue {
38 pub severity: IssueSeverity,
40 pub section: String,
42 pub rule: String,
44 pub explanation: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct CommitSuggestion {
51 pub message: String,
53 pub explanation: String,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "lowercase")]
60pub enum IssueSeverity {
61 Error,
63 Warning,
65 Info,
67}
68
69impl fmt::Display for IssueSeverity {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 match self {
72 IssueSeverity::Error => write!(f, "ERROR"),
73 IssueSeverity::Warning => write!(f, "WARNING"),
74 IssueSeverity::Info => write!(f, "INFO"),
75 }
76 }
77}
78
79impl std::str::FromStr for IssueSeverity {
80 type Err = ();
81
82 fn from_str(s: &str) -> Result<Self, Self::Err> {
83 match s.to_lowercase().as_str() {
84 "error" => Ok(IssueSeverity::Error),
85 "warning" => Ok(IssueSeverity::Warning),
86 "info" => Ok(IssueSeverity::Info),
87 other => {
88 tracing::debug!("Unknown severity {other:?}, defaulting to Warning");
89 Ok(IssueSeverity::Warning)
90 }
91 }
92 }
93}
94
95impl IssueSeverity {
96 #[must_use]
98 pub fn parse(s: &str) -> Self {
99 s.parse().expect("IssueSeverity::from_str is infallible")
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct CheckSummary {
107 pub total_commits: usize,
109 pub passing_commits: usize,
111 pub failing_commits: usize,
113 pub error_count: usize,
115 pub warning_count: usize,
117 pub info_count: usize,
119}
120
121impl CheckSummary {
122 pub fn from_results(results: &[CommitCheckResult]) -> Self {
124 let total_commits = results.len();
125 let passing_commits = results.iter().filter(|r| r.passes).count();
126 let failing_commits = total_commits - passing_commits;
127
128 let mut error_count = 0;
129 let mut warning_count = 0;
130 let mut info_count = 0;
131
132 for result in results {
133 for issue in &result.issues {
134 match issue.severity {
135 IssueSeverity::Error => error_count += 1,
136 IssueSeverity::Warning => warning_count += 1,
137 IssueSeverity::Info => info_count += 1,
138 }
139 }
140 }
141
142 Self {
143 total_commits,
144 passing_commits,
145 failing_commits,
146 error_count,
147 warning_count,
148 info_count,
149 }
150 }
151}
152
153impl CheckReport {
154 pub fn new(commits: Vec<CommitCheckResult>) -> Self {
156 let summary = CheckSummary::from_results(&commits);
157 Self { commits, summary }
158 }
159
160 #[must_use]
162 pub fn has_errors(&self) -> bool {
163 self.summary.error_count > 0
164 }
165
166 #[must_use]
168 pub fn has_warnings(&self) -> bool {
169 self.summary.warning_count > 0
170 }
171
172 pub fn exit_code(&self, strict: bool) -> i32 {
174 if self.has_errors() {
175 1
176 } else if strict && self.has_warnings() {
177 2
178 } else {
179 0
180 }
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
186pub enum OutputFormat {
187 #[default]
189 Text,
190 Json,
192 Yaml,
194}
195
196impl std::str::FromStr for OutputFormat {
197 type Err = ();
198
199 fn from_str(s: &str) -> Result<Self, Self::Err> {
200 match s.to_lowercase().as_str() {
201 "text" => Ok(OutputFormat::Text),
202 "json" => Ok(OutputFormat::Json),
203 "yaml" => Ok(OutputFormat::Yaml),
204 _ => Err(()),
205 }
206 }
207}
208
209impl fmt::Display for OutputFormat {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 OutputFormat::Text => write!(f, "text"),
213 OutputFormat::Json => write!(f, "json"),
214 OutputFormat::Yaml => write!(f, "yaml"),
215 }
216 }
217}
218
219#[derive(Debug, Clone, Deserialize)]
221pub struct AiCheckResponse {
222 pub checks: Vec<AiCommitCheck>,
224}
225
226#[derive(Debug, Clone, Deserialize)]
228pub struct AiCommitCheck {
229 pub commit: String,
231 pub passes: bool,
233 #[serde(default)]
235 pub issues: Vec<AiIssue>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub suggestion: Option<AiSuggestion>,
239 #[serde(default)]
241 pub summary: Option<String>,
242}
243
244#[derive(Debug, Clone, Deserialize)]
246pub struct AiIssue {
247 pub severity: String,
249 pub section: String,
251 pub rule: String,
253 pub explanation: String,
255}
256
257#[derive(Debug, Clone, Deserialize)]
259pub struct AiSuggestion {
260 pub message: String,
262 pub explanation: String,
264}
265
266impl From<AiCommitCheck> for CommitCheckResult {
267 fn from(ai: AiCommitCheck) -> Self {
268 let issues: Vec<CommitIssue> = ai
269 .issues
270 .into_iter()
271 .map(|i| CommitIssue {
272 severity: IssueSeverity::parse(&i.severity),
273 section: i.section,
274 rule: i.rule,
275 explanation: i.explanation,
276 })
277 .collect();
278
279 let suggestion = ai.suggestion.map(|s| CommitSuggestion {
280 message: s.message,
281 explanation: s.explanation,
282 });
283
284 Self {
285 hash: ai.commit,
286 message: String::new(), issues,
288 suggestion,
289 passes: ai.passes,
290 summary: ai.summary,
291 }
292 }
293}