1use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
14#[serde(rename_all = "lowercase")]
15pub enum Severity {
16 Info,
18 Warning,
20 Error,
22}
23
24impl Severity {
25 pub fn is_blocking(self) -> bool {
27 matches!(self, Self::Error)
28 }
29
30 pub fn as_str(self) -> &'static str {
32 match self {
33 Self::Info => "info",
34 Self::Warning => "warning",
35 Self::Error => "error",
36 }
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Violation {
43 pub rule_id: String,
45 pub severity: Severity,
47 pub file_path: PathBuf,
49 pub line_number: Option<u32>,
51 pub column_number: Option<u32>,
53 pub message: String,
55 pub context: Option<String>,
57 pub suggested_fix: Option<String>,
59 pub detected_at: DateTime<Utc>,
61}
62
63impl Violation {
64 pub fn new(
66 rule_id: impl Into<String>,
67 severity: Severity,
68 file_path: PathBuf,
69 message: impl Into<String>,
70 ) -> Self {
71 Self {
72 rule_id: rule_id.into(),
73 severity,
74 file_path,
75 line_number: None,
76 column_number: None,
77 message: message.into(),
78 context: None,
79 suggested_fix: None,
80 detected_at: Utc::now(),
81 }
82 }
83
84 pub fn with_position(mut self, line: u32, column: u32) -> Self {
86 self.line_number = Some(line);
87 self.column_number = Some(column);
88 self
89 }
90
91 pub fn with_context(mut self, context: impl Into<String>) -> Self {
93 self.context = Some(context.into());
94 self
95 }
96
97 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
99 self.suggested_fix = Some(suggestion.into());
100 self
101 }
102
103 pub fn is_blocking(&self) -> bool {
105 self.severity.is_blocking()
106 }
107
108 pub fn format_display(&self) -> String {
110 let location = match (self.line_number, self.column_number) {
111 (Some(line), Some(col)) => format!(":{line}:{col}"),
112 (Some(line), None) => format!(":{line}"),
113 _ => String::new(),
114 };
115
116 format!(
117 "{}{} [{}] {}",
118 self.file_path.display(),
119 location,
120 self.severity.as_str(),
121 self.message
122 )
123 }
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize)]
128pub struct ValidationSummary {
129 pub total_files: usize,
131 pub violations_by_severity: ViolationCounts,
133 pub execution_time_ms: u64,
135 pub validated_at: DateTime<Utc>,
137}
138
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
141pub struct ViolationCounts {
142 pub error: usize,
143 pub warning: usize,
144 pub info: usize,
145}
146
147impl ViolationCounts {
148 pub fn total(&self) -> usize {
150 self.error + self.warning + self.info
151 }
152
153 pub fn has_blocking(&self) -> bool {
155 self.error > 0
156 }
157
158 pub fn add(&mut self, severity: Severity) {
160 match severity {
161 Severity::Error => self.error += 1,
162 Severity::Warning => self.warning += 1,
163 Severity::Info => self.info += 1,
164 }
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct ValidationReport {
171 pub violations: Vec<Violation>,
173 pub summary: ValidationSummary,
175 pub config_fingerprint: Option<String>,
177}
178
179impl ValidationReport {
180 pub fn new() -> Self {
182 Self {
183 violations: Vec::new(),
184 summary: ValidationSummary { validated_at: Utc::now(), ..Default::default() },
185 config_fingerprint: None,
186 }
187 }
188
189 pub fn add_violation(&mut self, violation: Violation) {
191 self.summary.violations_by_severity.add(violation.severity);
192 self.violations.push(violation);
193 }
194
195 pub fn has_violations(&self) -> bool {
197 !self.violations.is_empty()
198 }
199
200 pub fn has_errors(&self) -> bool {
202 self.summary.violations_by_severity.has_blocking()
203 }
204
205 pub fn violations_by_severity(&self, severity: Severity) -> impl Iterator<Item = &Violation> {
207 self.violations.iter().filter(move |v| v.severity == severity)
208 }
209
210 pub fn set_files_analyzed(&mut self, count: usize) {
212 self.summary.total_files = count;
213 }
214
215 pub fn set_execution_time(&mut self, duration_ms: u64) {
217 self.summary.execution_time_ms = duration_ms;
218 }
219
220 pub fn set_config_fingerprint(&mut self, fingerprint: impl Into<String>) {
222 self.config_fingerprint = Some(fingerprint.into());
223 }
224
225 pub fn merge(&mut self, other: ValidationReport) {
227 for violation in other.violations {
228 self.add_violation(violation);
229 }
230 self.summary.total_files += other.summary.total_files;
231 }
232
233 pub fn sort_violations(&mut self) {
235 self.violations.sort_by(|a, b| {
236 a.file_path
237 .cmp(&b.file_path)
238 .then_with(|| a.line_number.unwrap_or(0).cmp(&b.line_number.unwrap_or(0)))
239 .then_with(|| a.severity.cmp(&b.severity))
240 });
241 }
242}
243
244impl Default for ValidationReport {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250#[derive(Debug, thiserror::Error)]
252pub enum GuardianError {
253 #[error("Configuration error: {message}")]
255 Configuration { message: String },
256
257 #[error("IO error: {source}")]
259 Io {
260 #[from]
261 source: std::io::Error,
262 },
263
264 #[error("Pattern error: {message}")]
266 Pattern { message: String },
267
268 #[error("Analysis error in {file}: {message}")]
270 Analysis { file: String, message: String },
271
272 #[error("Cache error: {message}")]
274 Cache { message: String },
275
276 #[error("Validation error: {message}")]
278 Validation { message: String },
279}
280
281impl GuardianError {
282 pub fn config(message: impl Into<String>) -> Self {
284 Self::Configuration { message: message.into() }
285 }
286
287 pub fn pattern(message: impl Into<String>) -> Self {
289 Self::Pattern { message: message.into() }
290 }
291
292 pub fn analysis(file: impl Into<String>, message: impl Into<String>) -> Self {
294 Self::Analysis { file: file.into(), message: message.into() }
295 }
296
297 pub fn cache(message: impl Into<String>) -> Self {
299 Self::Cache { message: message.into() }
300 }
301
302 pub fn validation(message: impl Into<String>) -> Self {
304 Self::Validation { message: message.into() }
305 }
306}
307
308pub type GuardianResult<T> = Result<T, GuardianError>;
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use std::path::Path;
315
316 #[test]
317 fn test_violation_creation() {
318 let violation = Violation::new(
319 "test_rule",
320 Severity::Error,
321 PathBuf::from("src/lib.rs"),
322 "Test message",
323 );
324
325 assert_eq!(violation.rule_id, "test_rule");
326 assert_eq!(violation.severity, Severity::Error);
327 assert_eq!(violation.file_path, Path::new("src/lib.rs"));
328 assert_eq!(violation.message, "Test message");
329 assert!(violation.is_blocking());
330 }
331
332 #[test]
333 fn test_violation_with_position() {
334 let violation = Violation::new(
335 "test_rule",
336 Severity::Warning,
337 PathBuf::from("src/lib.rs"),
338 "Test message",
339 )
340 .with_position(42, 15)
341 .with_context("let x = unimplemented!();");
342
343 assert_eq!(violation.line_number, Some(42));
344 assert_eq!(violation.column_number, Some(15));
345 assert_eq!(violation.context, Some("let x = unimplemented!();".to_string()));
346 assert!(!violation.is_blocking());
347 }
348
349 #[test]
350 fn test_validation_report() {
351 let mut report = ValidationReport::new();
352
353 report.add_violation(Violation::new(
354 "rule1",
355 Severity::Error,
356 PathBuf::from("src/main.rs"),
357 "Error message",
358 ));
359
360 report.add_violation(Violation::new(
361 "rule2",
362 Severity::Warning,
363 PathBuf::from("src/lib.rs"),
364 "Warning message",
365 ));
366
367 assert!(report.has_violations());
368 assert!(report.has_errors());
369 assert_eq!(report.summary.violations_by_severity.total(), 2);
370 assert_eq!(report.summary.violations_by_severity.error, 1);
371 assert_eq!(report.summary.violations_by_severity.warning, 1);
372 }
373
374 #[test]
375 fn test_severity_ordering() {
376 assert!(Severity::Error > Severity::Warning);
377 assert!(Severity::Warning > Severity::Info);
378 assert!(Severity::Error.is_blocking());
379 assert!(!Severity::Warning.is_blocking());
380 }
381}