use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Warning,
Error,
}
impl Severity {
pub fn is_blocking(self) -> bool {
matches!(self, Self::Error)
}
pub fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Error => "error",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Violation {
pub rule_id: String,
pub severity: Severity,
pub file_path: PathBuf,
pub line_number: Option<u32>,
pub column_number: Option<u32>,
pub message: String,
pub context: Option<String>,
pub suggested_fix: Option<String>,
pub detected_at: DateTime<Utc>,
}
impl Violation {
pub fn new(
rule_id: impl Into<String>,
severity: Severity,
file_path: PathBuf,
message: impl Into<String>,
) -> Self {
Self {
rule_id: rule_id.into(),
severity,
file_path,
line_number: None,
column_number: None,
message: message.into(),
context: None,
suggested_fix: None,
detected_at: Utc::now(),
}
}
pub fn with_position(mut self, line: u32, column: u32) -> Self {
self.line_number = Some(line);
self.column_number = Some(column);
self
}
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = Some(context.into());
self
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggested_fix = Some(suggestion.into());
self
}
pub fn is_blocking(&self) -> bool {
self.severity.is_blocking()
}
pub fn format_display(&self) -> String {
let location = match (self.line_number, self.column_number) {
(Some(line), Some(col)) => format!(":{line}:{col}"),
(Some(line), None) => format!(":{line}"),
_ => String::new(),
};
format!(
"{}{} [{}] {}",
self.file_path.display(),
location,
self.severity.as_str(),
self.message
)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ValidationSummary {
pub total_files: usize,
pub violations_by_severity: ViolationCounts,
pub execution_time_ms: u64,
pub validated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ViolationCounts {
pub error: usize,
pub warning: usize,
pub info: usize,
}
impl ViolationCounts {
pub fn total(&self) -> usize {
self.error + self.warning + self.info
}
pub fn has_blocking(&self) -> bool {
self.error > 0
}
pub fn add(&mut self, severity: Severity) {
match severity {
Severity::Error => self.error += 1,
Severity::Warning => self.warning += 1,
Severity::Info => self.info += 1,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationReport {
pub violations: Vec<Violation>,
pub summary: ValidationSummary,
pub config_fingerprint: Option<String>,
}
impl ValidationReport {
pub fn new() -> Self {
Self {
violations: Vec::new(),
summary: ValidationSummary {
validated_at: Utc::now(),
..Default::default()
},
config_fingerprint: None,
}
}
pub fn add_violation(&mut self, violation: Violation) {
self.summary.violations_by_severity.add(violation.severity);
self.violations.push(violation);
}
pub fn has_violations(&self) -> bool {
!self.violations.is_empty()
}
pub fn has_errors(&self) -> bool {
self.summary.violations_by_severity.has_blocking()
}
pub fn violations_by_severity(&self, severity: Severity) -> impl Iterator<Item = &Violation> {
self.violations
.iter()
.filter(move |v| v.severity == severity)
}
pub fn set_files_analyzed(&mut self, count: usize) {
self.summary.total_files = count;
}
pub fn set_execution_time(&mut self, duration_ms: u64) {
self.summary.execution_time_ms = duration_ms;
}
pub fn set_config_fingerprint(&mut self, fingerprint: impl Into<String>) {
self.config_fingerprint = Some(fingerprint.into());
}
pub fn merge(&mut self, other: ValidationReport) {
for violation in other.violations {
self.add_violation(violation);
}
self.summary.total_files += other.summary.total_files;
}
pub fn sort_violations(&mut self) {
self.violations.sort_by(|a, b| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.line_number.unwrap_or(0).cmp(&b.line_number.unwrap_or(0)))
.then_with(|| a.severity.cmp(&b.severity))
});
}
}
impl Default for ValidationReport {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, thiserror::Error)]
pub enum GuardianError {
#[error("Configuration error: {message}")]
Configuration { message: String },
#[error("IO error: {source}")]
Io {
#[from]
source: std::io::Error,
},
#[error("Pattern error: {message}")]
Pattern { message: String },
#[error("Analysis error in {file}: {message}")]
Analysis { file: String, message: String },
#[error("Cache error: {message}")]
Cache { message: String },
#[error("Validation error: {message}")]
Validation { message: String },
}
impl GuardianError {
pub fn config(message: impl Into<String>) -> Self {
Self::Configuration {
message: message.into(),
}
}
pub fn pattern(message: impl Into<String>) -> Self {
Self::Pattern {
message: message.into(),
}
}
pub fn analysis(file: impl Into<String>, message: impl Into<String>) -> Self {
Self::Analysis {
file: file.into(),
message: message.into(),
}
}
pub fn cache(message: impl Into<String>) -> Self {
Self::Cache {
message: message.into(),
}
}
pub fn validation(message: impl Into<String>) -> Self {
Self::Validation {
message: message.into(),
}
}
}
pub type GuardianResult<T> = Result<T, GuardianError>;
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_violation_creation() {
let violation = Violation::new(
"test_rule",
Severity::Error,
PathBuf::from("src/lib.rs"),
"Test message",
);
assert_eq!(violation.rule_id, "test_rule");
assert_eq!(violation.severity, Severity::Error);
assert_eq!(violation.file_path, Path::new("src/lib.rs"));
assert_eq!(violation.message, "Test message");
assert!(violation.is_blocking());
}
#[test]
fn test_violation_with_position() {
let violation = Violation::new(
"test_rule",
Severity::Warning,
PathBuf::from("src/lib.rs"),
"Test message",
)
.with_position(42, 15)
.with_context("let x = unimplemented!();");
assert_eq!(violation.line_number, Some(42));
assert_eq!(violation.column_number, Some(15));
assert_eq!(
violation.context,
Some("let x = unimplemented!();".to_string())
);
assert!(!violation.is_blocking());
}
#[test]
fn test_validation_report() {
let mut report = ValidationReport::new();
report.add_violation(Violation::new(
"rule1",
Severity::Error,
PathBuf::from("src/main.rs"),
"Error message",
));
report.add_violation(Violation::new(
"rule2",
Severity::Warning,
PathBuf::from("src/lib.rs"),
"Warning message",
));
assert!(report.has_violations());
assert!(report.has_errors());
assert_eq!(report.summary.violations_by_severity.total(), 2);
assert_eq!(report.summary.violations_by_severity.error, 1);
assert_eq!(report.summary.violations_by_severity.warning, 1);
}
#[test]
fn test_severity_ordering() {
assert!(Severity::Error > Severity::Warning);
assert!(Severity::Warning > Severity::Info);
assert!(Severity::Error.is_blocking());
assert!(!Severity::Warning.is_blocking());
}
}