use crate::{AuditError, IssueSeverity, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Write;
use std::io::Write as IoWrite;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditReport {
pub summary: AuditSummary,
pub file_results: Vec<FileAuditResult>,
pub issues: Vec<AuditIssue>,
pub recommendations: Vec<Recommendation>,
pub timestamp: DateTime<Utc>,
pub audit_config: AuditReportConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditSummary {
pub total_files: usize,
pub files_with_issues: usize,
pub total_issues: usize,
pub critical_issues: usize,
pub warning_issues: usize,
pub info_issues: usize,
pub coverage_percentage: f64,
pub average_issues_per_file: f64,
pub most_common_issue: Option<IssueCategory>,
pub problematic_files: Vec<ProblematicFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProblematicFile {
pub path: PathBuf,
pub issue_count: usize,
pub max_severity: IssueSeverity,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileAuditResult {
pub file_path: PathBuf,
pub file_hash: String,
pub last_modified: DateTime<Utc>,
pub issues_count: usize,
pub issues: Vec<AuditIssue>,
pub passed: bool,
pub audit_duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditIssue {
pub id: String,
pub file_path: PathBuf,
pub line_number: Option<usize>,
pub column_number: Option<usize>,
pub severity: IssueSeverity,
pub category: IssueCategory,
pub message: String,
pub suggestion: Option<String>,
pub context: Option<String>,
pub code_snippet: Option<String>,
pub related_issues: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum IssueCategory {
ApiMismatch,
VersionInconsistency,
CompilationError,
BrokenLink,
MissingDocumentation,
DeprecatedApi,
InvalidImport,
ConfigurationError,
AsyncPatternError,
InvalidFeatureFlag,
InvalidCrateName,
QualityIssue,
ProcessingError,
ValidationError,
}
impl IssueCategory {
pub fn description(&self) -> &'static str {
match self {
IssueCategory::ApiMismatch => "API reference doesn't match implementation",
IssueCategory::VersionInconsistency => "Version numbers are inconsistent",
IssueCategory::CompilationError => "Code example fails to compile",
IssueCategory::BrokenLink => "Internal link is broken",
IssueCategory::MissingDocumentation => "Missing documentation for feature",
IssueCategory::DeprecatedApi => "References deprecated API",
IssueCategory::InvalidImport => "Import statement is invalid",
IssueCategory::ConfigurationError => "Configuration parameter error",
IssueCategory::AsyncPatternError => "Async/await pattern is incorrect",
IssueCategory::InvalidFeatureFlag => "Feature flag reference is invalid",
IssueCategory::InvalidCrateName => "Crate name reference is invalid",
IssueCategory::QualityIssue => "General documentation quality issue",
IssueCategory::ProcessingError => "Error occurred while processing file",
IssueCategory::ValidationError => "Error occurred during validation",
}
}
pub fn default_severity(&self) -> IssueSeverity {
match self {
IssueCategory::ApiMismatch => IssueSeverity::Critical,
IssueCategory::CompilationError => IssueSeverity::Critical,
IssueCategory::VersionInconsistency => IssueSeverity::Warning,
IssueCategory::BrokenLink => IssueSeverity::Warning,
IssueCategory::DeprecatedApi => IssueSeverity::Warning,
IssueCategory::InvalidImport => IssueSeverity::Critical,
IssueCategory::ConfigurationError => IssueSeverity::Warning,
IssueCategory::AsyncPatternError => IssueSeverity::Warning,
IssueCategory::InvalidFeatureFlag => IssueSeverity::Warning,
IssueCategory::InvalidCrateName => IssueSeverity::Warning,
IssueCategory::MissingDocumentation => IssueSeverity::Info,
IssueCategory::QualityIssue => IssueSeverity::Info,
IssueCategory::ProcessingError => IssueSeverity::Critical,
IssueCategory::ValidationError => IssueSeverity::Warning,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recommendation {
pub id: String,
pub recommendation_type: RecommendationType,
pub priority: u8,
pub title: String,
pub description: String,
pub affected_files: Vec<PathBuf>,
pub estimated_effort_hours: Option<f32>,
pub resolves_issues: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RecommendationType {
FixIssue,
StructuralImprovement,
AddDocumentation,
UpdateContent,
ImproveExamples,
EnhanceCrossReferences,
ProcessImprovement,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditReportConfig {
pub min_severity: IssueSeverity,
pub include_suggestions: bool,
pub include_code_snippets: bool,
pub max_issues_per_file: Option<usize>,
pub include_statistics: bool,
pub include_recommendations: bool,
}
impl Default for AuditReportConfig {
fn default() -> Self {
Self {
min_severity: IssueSeverity::Info,
include_suggestions: true,
include_code_snippets: true,
max_issues_per_file: None,
include_statistics: true,
include_recommendations: true,
}
}
}
impl AuditReport {
pub fn new(config: AuditReportConfig) -> Self {
Self {
summary: AuditSummary::default(),
file_results: Vec::new(),
issues: Vec::new(),
recommendations: Vec::new(),
timestamp: Utc::now(),
audit_config: config,
}
}
pub fn add_file_result(&mut self, file_result: FileAuditResult) {
self.issues.extend(file_result.issues.clone());
self.file_results.push(file_result);
}
pub fn add_issue(&mut self, issue: AuditIssue) {
if issue.severity >= self.audit_config.min_severity {
self.issues.push(issue);
}
}
pub fn add_recommendation(&mut self, recommendation: Recommendation) {
if self.audit_config.include_recommendations {
self.recommendations.push(recommendation);
}
}
pub fn calculate_summary(&mut self) {
let total_files = self.file_results.len();
let files_with_issues = self.file_results.iter().filter(|f| f.issues_count > 0).count();
let total_issues = self.issues.len();
let critical_issues =
self.issues.iter().filter(|i| i.severity == IssueSeverity::Critical).count();
let warning_issues =
self.issues.iter().filter(|i| i.severity == IssueSeverity::Warning).count();
let info_issues = self.issues.iter().filter(|i| i.severity == IssueSeverity::Info).count();
let files_without_critical = self
.file_results
.iter()
.filter(|f| !f.issues.iter().any(|i| i.severity == IssueSeverity::Critical))
.count();
let coverage_percentage = if total_files > 0 {
(files_without_critical as f64 / total_files as f64) * 100.0
} else {
100.0
};
let average_issues_per_file =
if total_files > 0 { total_issues as f64 / total_files as f64 } else { 0.0 };
let mut category_counts: HashMap<IssueCategory, usize> = HashMap::new();
for issue in &self.issues {
*category_counts.entry(issue.category).or_insert(0) += 1;
}
let most_common_issue = category_counts
.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(category, _)| category);
let mut file_issue_counts: Vec<_> = self
.file_results
.iter()
.map(|f| ProblematicFile {
path: f.file_path.clone(),
issue_count: f.issues_count,
max_severity: f
.issues
.iter()
.map(|i| i.severity)
.max()
.unwrap_or(IssueSeverity::Info),
})
.collect();
file_issue_counts.sort_by(|a, b| b.issue_count.cmp(&a.issue_count));
file_issue_counts.truncate(5);
self.summary = AuditSummary {
total_files,
files_with_issues,
total_issues,
critical_issues,
warning_issues,
info_issues,
coverage_percentage,
average_issues_per_file,
most_common_issue,
problematic_files: file_issue_counts,
};
}
pub fn passed(&self) -> bool {
self.summary.critical_issues == 0
}
pub fn issues_by_category(&self) -> HashMap<IssueCategory, Vec<&AuditIssue>> {
let mut categorized = HashMap::new();
for issue in &self.issues {
categorized.entry(issue.category).or_insert_with(Vec::new).push(issue);
}
categorized
}
pub fn issues_by_severity(&self) -> HashMap<IssueSeverity, Vec<&AuditIssue>> {
let mut by_severity = HashMap::new();
for issue in &self.issues {
by_severity.entry(issue.severity).or_insert_with(Vec::new).push(issue);
}
by_severity
}
pub fn issues_for_file(&self, file_path: &PathBuf) -> Vec<&AuditIssue> {
self.issues.iter().filter(|issue| &issue.file_path == file_path).collect()
}
}
impl Default for AuditSummary {
fn default() -> Self {
Self {
total_files: 0,
files_with_issues: 0,
total_issues: 0,
critical_issues: 0,
warning_issues: 0,
info_issues: 0,
coverage_percentage: 100.0,
average_issues_per_file: 0.0,
most_common_issue: None,
problematic_files: Vec::new(),
}
}
}
impl AuditIssue {
pub fn new(file_path: PathBuf, category: IssueCategory, message: String) -> Self {
let id = uuid::Uuid::new_v4().to_string();
let severity = category.default_severity();
Self {
id,
file_path,
line_number: None,
column_number: None,
severity,
category,
message,
suggestion: None,
context: None,
code_snippet: None,
related_issues: Vec::new(),
}
}
pub fn with_line_number(mut self, line_number: usize) -> Self {
self.line_number = Some(line_number);
self
}
pub fn with_column_number(mut self, column_number: usize) -> Self {
self.column_number = Some(column_number);
self
}
pub fn with_severity(mut self, severity: IssueSeverity) -> Self {
self.severity = severity;
self
}
pub fn with_suggestion(mut self, suggestion: String) -> Self {
self.suggestion = Some(suggestion);
self
}
pub fn with_context(mut self, context: String) -> Self {
self.context = Some(context);
self
}
pub fn with_code_snippet(mut self, code_snippet: String) -> Self {
self.code_snippet = Some(code_snippet);
self
}
pub fn with_related_issue(mut self, issue_id: String) -> Self {
self.related_issues.push(issue_id);
self
}
}
impl Recommendation {
pub fn new(
recommendation_type: RecommendationType,
title: String,
description: String,
) -> Self {
let id = uuid::Uuid::new_v4().to_string();
Self {
id,
recommendation_type,
priority: 3, title,
description,
affected_files: Vec::new(),
estimated_effort_hours: None,
resolves_issues: Vec::new(),
}
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = priority.clamp(1, 5);
self
}
pub fn with_affected_file(mut self, file_path: PathBuf) -> Self {
self.affected_files.push(file_path);
self
}
pub fn with_estimated_effort(mut self, hours: f32) -> Self {
self.estimated_effort_hours = Some(hours);
self
}
pub fn resolves_issue(mut self, issue_id: String) -> Self {
self.resolves_issues.push(issue_id);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_report_creation() {
let config = AuditReportConfig::default();
let report = AuditReport::new(config);
assert_eq!(report.summary.total_files, 0);
assert_eq!(report.issues.len(), 0);
assert!(report.passed());
}
#[test]
fn test_issue_creation() {
let issue = AuditIssue::new(
PathBuf::from("test.md"),
IssueCategory::ApiMismatch,
"Test issue".to_string(),
)
.with_line_number(42)
.with_suggestion("Fix this".to_string());
assert_eq!(issue.file_path, PathBuf::from("test.md"));
assert_eq!(issue.category, IssueCategory::ApiMismatch);
assert_eq!(issue.severity, IssueSeverity::Critical);
assert_eq!(issue.line_number, Some(42));
assert_eq!(issue.suggestion, Some("Fix this".to_string()));
}
#[test]
fn test_issue_category_descriptions() {
assert_eq!(
IssueCategory::ApiMismatch.description(),
"API reference doesn't match implementation"
);
assert_eq!(IssueCategory::CompilationError.description(), "Code example fails to compile");
}
#[test]
fn test_issue_category_default_severity() {
assert_eq!(IssueCategory::ApiMismatch.default_severity(), IssueSeverity::Critical);
assert_eq!(IssueCategory::VersionInconsistency.default_severity(), IssueSeverity::Warning);
assert_eq!(IssueCategory::MissingDocumentation.default_severity(), IssueSeverity::Info);
}
#[test]
fn test_recommendation_creation() {
let rec = Recommendation::new(
RecommendationType::FixIssue,
"Fix API references".to_string(),
"Update all API references to match current implementation".to_string(),
)
.with_priority(1)
.with_estimated_effort(2.5);
assert_eq!(rec.recommendation_type, RecommendationType::FixIssue);
assert_eq!(rec.priority, 1);
assert_eq!(rec.estimated_effort_hours, Some(2.5));
}
#[test]
fn test_audit_summary_calculation() {
let mut report = AuditReport::new(AuditReportConfig::default());
let file1 = FileAuditResult {
file_path: PathBuf::from("file1.md"),
file_hash: "hash1".to_string(),
last_modified: Utc::now(),
issues_count: 2,
issues: vec![
AuditIssue::new(
PathBuf::from("file1.md"),
IssueCategory::ApiMismatch,
"Issue 1".to_string(),
),
AuditIssue::new(
PathBuf::from("file1.md"),
IssueCategory::VersionInconsistency,
"Issue 2".to_string(),
),
],
passed: false,
audit_duration_ms: 100,
};
let file2 = FileAuditResult {
file_path: PathBuf::from("file2.md"),
file_hash: "hash2".to_string(),
last_modified: Utc::now(),
issues_count: 0,
issues: vec![],
passed: true,
audit_duration_ms: 50,
};
report.add_file_result(file1);
report.add_file_result(file2);
report.calculate_summary();
assert_eq!(report.summary.total_files, 2);
assert_eq!(report.summary.files_with_issues, 1);
assert_eq!(report.summary.total_issues, 2);
assert_eq!(report.summary.critical_issues, 1);
assert_eq!(report.summary.warning_issues, 1);
assert_eq!(report.summary.coverage_percentage, 50.0); }
#[test]
fn test_issues_by_category() {
let mut report = AuditReport::new(AuditReportConfig::default());
report.add_issue(AuditIssue::new(
PathBuf::from("test.md"),
IssueCategory::ApiMismatch,
"API issue".to_string(),
));
report.add_issue(AuditIssue::new(
PathBuf::from("test.md"),
IssueCategory::ApiMismatch,
"Another API issue".to_string(),
));
report.add_issue(AuditIssue::new(
PathBuf::from("test.md"),
IssueCategory::CompilationError,
"Compilation issue".to_string(),
));
let by_category = report.issues_by_category();
assert_eq!(by_category.get(&IssueCategory::ApiMismatch).unwrap().len(), 2);
assert_eq!(by_category.get(&IssueCategory::CompilationError).unwrap().len(), 1);
}
#[test]
fn test_report_generator_json() {
let mut report = AuditReport::new(AuditReportConfig::default());
report.add_issue(AuditIssue::new(
PathBuf::from("test.md"),
IssueCategory::ApiMismatch,
"Test issue".to_string(),
));
report.calculate_summary();
let generator = ReportGenerator::new(OutputFormat::Json);
let json_output = generator.generate_report_string(&report).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json_output).unwrap();
assert!(parsed.get("summary").is_some());
assert!(parsed.get("issues").is_some());
assert!(parsed.get("timestamp").is_some());
}
#[test]
fn test_report_generator_markdown() {
let mut report = AuditReport::new(AuditReportConfig::default());
report.add_issue(
AuditIssue::new(
PathBuf::from("test.md"),
IssueCategory::CompilationError,
"Compilation failed".to_string(),
)
.with_line_number(42),
);
report.calculate_summary();
let generator = ReportGenerator::new(OutputFormat::Markdown);
let markdown_output = generator.generate_report_string(&report).unwrap();
assert!(markdown_output.contains("# Documentation Audit Report"));
assert!(markdown_output.contains("## Executive Summary"));
assert!(markdown_output.contains("test.md"));
assert!(markdown_output.contains("line 42"));
}
#[test]
fn test_report_generator_console() {
let mut report = AuditReport::new(AuditReportConfig::default());
report.add_issue(AuditIssue::new(
PathBuf::from("test.md"),
IssueCategory::VersionInconsistency,
"Version mismatch".to_string(),
));
report.calculate_summary();
let generator = ReportGenerator::new(OutputFormat::Console);
let console_output = generator.generate_report_string(&report).unwrap();
assert!(console_output.contains("DOCUMENTATION AUDIT REPORT"));
assert!(console_output.contains("SUMMARY"));
assert!(console_output.contains("Total Files:"));
assert!(console_output.contains("🟡 WARNING"));
}
#[test]
fn test_wrap_text() {
use super::wrap_text;
let text = "This is a very long line that should be wrapped at the specified width";
let wrapped = wrap_text(text, 20);
for line in wrapped.lines() {
assert!(line.len() <= 20);
}
let original_words: Vec<&str> = text.split_whitespace().collect();
let wrapped_words: Vec<&str> = wrapped.split_whitespace().collect();
assert_eq!(original_words, wrapped_words);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OutputFormat {
Json,
Markdown,
Console,
}
impl From<crate::config::OutputFormat> for OutputFormat {
fn from(config_format: crate::config::OutputFormat) -> Self {
match config_format {
crate::config::OutputFormat::Console => OutputFormat::Console,
crate::config::OutputFormat::Json => OutputFormat::Json,
crate::config::OutputFormat::Markdown => OutputFormat::Markdown,
}
}
}
pub struct ReportGenerator {
output_format: OutputFormat,
config: AuditReportConfig,
}
impl ReportGenerator {
pub fn new(output_format: OutputFormat) -> Self {
Self { output_format, config: AuditReportConfig::default() }
}
pub fn with_config(output_format: OutputFormat, config: AuditReportConfig) -> Self {
Self { output_format, config }
}
pub fn generate_report<W: IoWrite>(&self, report: &AuditReport, writer: &mut W) -> Result<()> {
match self.output_format {
OutputFormat::Json => self.generate_json_report(report, writer),
OutputFormat::Markdown => self.generate_markdown_report(report, writer),
OutputFormat::Console => self.generate_console_report(report, writer),
}
}
pub fn generate_report_string(&self, report: &AuditReport) -> Result<String> {
let mut buffer = Vec::new();
self.generate_report(report, &mut buffer)?;
String::from_utf8(buffer).map_err(|e| AuditError::ReportGeneration {
details: format!("UTF-8 conversion error: {}", e),
})
}
fn generate_json_report<W: IoWrite>(&self, report: &AuditReport, writer: &mut W) -> Result<()> {
let json = if self.config.include_statistics {
serde_json::to_string_pretty(report)
} else {
let simplified = SimplifiedReport {
summary: &report.summary,
issues: &report.issues,
recommendations: if self.config.include_recommendations {
Some(&report.recommendations)
} else {
None
},
timestamp: report.timestamp,
};
serde_json::to_string_pretty(&simplified)
};
let json = json.map_err(|e| AuditError::ReportGeneration {
details: format!("JSON serialization error: {}", e),
})?;
writer
.write_all(json.as_bytes())
.map_err(|e| AuditError::ReportGeneration { details: format!("Write error: {}", e) })?;
Ok(())
}
fn generate_markdown_report<W: IoWrite>(
&self,
report: &AuditReport,
writer: &mut W,
) -> Result<()> {
let mut output = String::new();
writeln!(output, "# Documentation Audit Report").unwrap();
writeln!(output).unwrap();
writeln!(output, "**Generated:** {}", report.timestamp.format("%Y-%m-%d %H:%M:%S UTC"))
.unwrap();
writeln!(output, "**Status:** {}", if report.passed() { "✅ PASSED" } else { "❌ FAILED" })
.unwrap();
writeln!(output).unwrap();
writeln!(output, "## Executive Summary").unwrap();
writeln!(output).unwrap();
writeln!(output, "- **Total Files Audited:** {}", report.summary.total_files).unwrap();
writeln!(output, "- **Files with Issues:** {}", report.summary.files_with_issues).unwrap();
writeln!(output, "- **Total Issues:** {}", report.summary.total_issues).unwrap();
writeln!(output, "- **Critical Issues:** {}", report.summary.critical_issues).unwrap();
writeln!(output, "- **Warning Issues:** {}", report.summary.warning_issues).unwrap();
writeln!(output, "- **Info Issues:** {}", report.summary.info_issues).unwrap();
writeln!(
output,
"- **Documentation Coverage:** {:.1}%",
report.summary.coverage_percentage
)
.unwrap();
writeln!(output).unwrap();
if !report.issues.is_empty() {
writeln!(output, "## Issues by Category").unwrap();
writeln!(output).unwrap();
let issues_by_category = report.issues_by_category();
for (category, issues) in issues_by_category {
writeln!(output, "### {} ({} issues)", category.description(), issues.len())
.unwrap();
writeln!(output).unwrap();
for issue in
issues.iter().take(self.config.max_issues_per_file.unwrap_or(usize::MAX))
{
let severity_icon = match issue.severity {
IssueSeverity::Critical => "🔴",
IssueSeverity::Warning => "🟡",
IssueSeverity::Info => "🔵",
};
write!(
output,
"- {} **{}**: {}",
severity_icon,
issue.file_path.display(),
issue.message
)
.unwrap();
if let Some(line) = issue.line_number {
write!(output, " (line {})", line).unwrap();
}
writeln!(output).unwrap();
if self.config.include_suggestions {
if let Some(suggestion) = &issue.suggestion {
writeln!(output, " - *Suggestion:* {}", suggestion).unwrap();
}
}
if self.config.include_code_snippets {
if let Some(snippet) = &issue.code_snippet {
writeln!(output, " ```").unwrap();
writeln!(output, " {}", snippet).unwrap();
writeln!(output, " ```").unwrap();
}
}
}
writeln!(output).unwrap();
}
}
if !report.summary.problematic_files.is_empty() {
writeln!(output, "## Most Problematic Files").unwrap();
writeln!(output).unwrap();
for (i, file) in report.summary.problematic_files.iter().enumerate() {
let severity_icon = match file.max_severity {
IssueSeverity::Critical => "🔴",
IssueSeverity::Warning => "🟡",
IssueSeverity::Info => "🔵",
};
writeln!(
output,
"{}. {} {} ({} issues)",
i + 1,
severity_icon,
file.path.display(),
file.issue_count
)
.unwrap();
}
writeln!(output).unwrap();
}
if self.config.include_recommendations && !report.recommendations.is_empty() {
writeln!(output, "## Recommendations").unwrap();
writeln!(output).unwrap();
let mut sorted_recommendations = report.recommendations.clone();
sorted_recommendations.sort_by_key(|r| r.priority);
for rec in sorted_recommendations {
let priority_text = match rec.priority {
1 => "🔴 High",
2 => "🟡 Medium-High",
3 => "🟡 Medium",
4 => "🔵 Medium-Low",
5 => "🔵 Low",
_ => "🔵 Low",
};
writeln!(output, "### {} - {}", priority_text, rec.title).unwrap();
writeln!(output).unwrap();
writeln!(output, "{}", rec.description).unwrap();
if let Some(effort) = rec.estimated_effort_hours {
writeln!(output, "**Estimated Effort:** {:.1} hours", effort).unwrap();
}
if !rec.affected_files.is_empty() {
writeln!(output, "**Affected Files:**").unwrap();
for file in &rec.affected_files {
writeln!(output, "- {}", file.display()).unwrap();
}
}
writeln!(output).unwrap();
}
}
writer
.write_all(output.as_bytes())
.map_err(|e| AuditError::ReportGeneration { details: format!("Write error: {}", e) })?;
Ok(())
}
fn generate_console_report<W: IoWrite>(
&self,
report: &AuditReport,
writer: &mut W,
) -> Result<()> {
let mut output = String::new();
writeln!(
output,
"╔══════════════════════════════════════════════════════════════════════════════╗"
)
.unwrap();
writeln!(
output,
"║ DOCUMENTATION AUDIT REPORT ║"
)
.unwrap();
writeln!(
output,
"╚══════════════════════════════════════════════════════════════════════════════╝"
)
.unwrap();
writeln!(output).unwrap();
let status = if report.passed() { "✅ PASSED" } else { "❌ FAILED" };
writeln!(output, "Status: {}", status).unwrap();
writeln!(output, "Generated: {}", report.timestamp.format("%Y-%m-%d %H:%M:%S UTC"))
.unwrap();
writeln!(output).unwrap();
writeln!(
output,
"┌─ SUMMARY ─────────────────────────────────────────────────────────────────────┐"
)
.unwrap();
writeln!(
output,
"│ Total Files: {:>8} │",
report.summary.total_files
)
.unwrap();
writeln!(
output,
"│ Files with Issues: {:>8} │",
report.summary.files_with_issues
)
.unwrap();
writeln!(
output,
"│ Total Issues: {:>8} │",
report.summary.total_issues
)
.unwrap();
writeln!(
output,
"│ Critical Issues: {:>8} │",
report.summary.critical_issues
)
.unwrap();
writeln!(
output,
"│ Warning Issues: {:>8} │",
report.summary.warning_issues
)
.unwrap();
writeln!(
output,
"│ Info Issues: {:>8} │",
report.summary.info_issues
)
.unwrap();
writeln!(
output,
"│ Coverage: {:>7.1}% │",
report.summary.coverage_percentage
)
.unwrap();
writeln!(
output,
"└───────────────────────────────────────────────────────────────────────────────┘"
)
.unwrap();
writeln!(output).unwrap();
if report.summary.total_issues > 0 {
writeln!(output, "ISSUES BY SEVERITY:").unwrap();
writeln!(
output,
"─────────────────────────────────────────────────────────────────────────────"
)
.unwrap();
let issues_by_severity = report.issues_by_severity();
if let Some(critical_issues) = issues_by_severity.get(&IssueSeverity::Critical) {
writeln!(output, "🔴 CRITICAL ({}):", critical_issues.len()).unwrap();
for issue in critical_issues.iter().take(5) {
writeln!(output, " {} - {}", issue.file_path.display(), issue.message)
.unwrap();
}
if critical_issues.len() > 5 {
writeln!(output, " ... and {} more", critical_issues.len() - 5).unwrap();
}
writeln!(output).unwrap();
}
if let Some(warning_issues) = issues_by_severity.get(&IssueSeverity::Warning) {
writeln!(output, "🟡 WARNING ({}):", warning_issues.len()).unwrap();
for issue in warning_issues.iter().take(3) {
writeln!(output, " {} - {}", issue.file_path.display(), issue.message)
.unwrap();
}
if warning_issues.len() > 3 {
writeln!(output, " ... and {} more", warning_issues.len() - 3).unwrap();
}
writeln!(output).unwrap();
}
if let Some(info_issues) = issues_by_severity.get(&IssueSeverity::Info) {
writeln!(output, "🔵 INFO ({}):", info_issues.len()).unwrap();
for issue in info_issues.iter().take(2) {
writeln!(output, " {} - {}", issue.file_path.display(), issue.message)
.unwrap();
}
if info_issues.len() > 2 {
writeln!(output, " ... and {} more", info_issues.len() - 2).unwrap();
}
writeln!(output).unwrap();
}
}
if !report.summary.problematic_files.is_empty() {
writeln!(output, "MOST PROBLEMATIC FILES:").unwrap();
writeln!(
output,
"─────────────────────────────────────────────────────────────────────────────"
)
.unwrap();
for (i, file) in report.summary.problematic_files.iter().enumerate() {
let severity_icon = match file.max_severity {
IssueSeverity::Critical => "🔴",
IssueSeverity::Warning => "🟡",
IssueSeverity::Info => "🔵",
};
writeln!(
output,
"{}. {} {} ({} issues)",
i + 1,
severity_icon,
file.path.display(),
file.issue_count
)
.unwrap();
}
writeln!(output).unwrap();
}
if self.config.include_recommendations && !report.recommendations.is_empty() {
writeln!(output, "TOP RECOMMENDATIONS:").unwrap();
writeln!(
output,
"─────────────────────────────────────────────────────────────────────────────"
)
.unwrap();
let mut sorted_recommendations = report.recommendations.clone();
sorted_recommendations.sort_by_key(|r| r.priority);
for rec in sorted_recommendations.iter().take(3) {
let priority_text = match rec.priority {
1 => "🔴 HIGH",
2 => "🟡 MED-HIGH",
3 => "🟡 MEDIUM",
4 => "🔵 MED-LOW",
5 => "🔵 LOW",
_ => "🔵 LOW",
};
writeln!(output, "{}: {}", priority_text, rec.title).unwrap();
let wrapped_desc = wrap_text(&rec.description, 75);
for line in wrapped_desc.lines() {
writeln!(output, " {}", line).unwrap();
}
writeln!(output).unwrap();
}
if report.recommendations.len() > 3 {
writeln!(
output,
"... and {} more recommendations",
report.recommendations.len() - 3
)
.unwrap();
writeln!(output).unwrap();
}
}
writeln!(
output,
"─────────────────────────────────────────────────────────────────────────────"
)
.unwrap();
if report.passed() {
writeln!(output, "✅ Audit completed successfully! No critical issues found.").unwrap();
} else {
writeln!(output, "❌ Audit failed. Please address critical issues before proceeding.")
.unwrap();
}
writer
.write_all(output.as_bytes())
.map_err(|e| AuditError::ReportGeneration { details: format!("Write error: {}", e) })?;
Ok(())
}
pub fn save_to_file(&self, report: &AuditReport, file_path: &std::path::Path) -> Result<()> {
use std::fs::File;
use std::io::BufWriter;
let file = File::create(file_path).map_err(|e| AuditError::IoError {
path: file_path.to_path_buf(),
details: format!("Failed to create report file: {}", e),
})?;
let mut writer = BufWriter::new(file);
self.generate_report(report, &mut writer)?;
Ok(())
}
}
#[derive(Serialize)]
struct SimplifiedReport<'a> {
summary: &'a AuditSummary,
issues: &'a [AuditIssue],
#[serde(skip_serializing_if = "Option::is_none")]
recommendations: Option<&'a [Recommendation]>,
timestamp: DateTime<Utc>,
}
fn wrap_text(text: &str, width: usize) -> String {
let mut result = String::new();
let mut current_line = String::new();
for word in text.split_whitespace() {
if current_line.len() + word.len() + 1 > width && !current_line.is_empty() {
result.push_str(¤t_line);
result.push('\n');
current_line.clear();
}
if !current_line.is_empty() {
current_line.push(' ');
}
current_line.push_str(word);
}
if !current_line.is_empty() {
result.push_str(¤t_line);
}
result
}