1use crate::comply::rule::{FixResult, RuleResult, RuleViolation, ViolationLevel};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt::Write as FmtWrite;
9
10#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
12pub enum ComplyReportFormat {
13 #[default]
14 Text,
15 Json,
16 Markdown,
17 Html,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ComplyReport {
23 pub results: HashMap<String, HashMap<String, ProjectRuleResult>>,
25 pub exemptions: Vec<Exemption>,
27 pub errors: Vec<String>,
29 pub summary: ComplianceSummary,
31 #[serde(skip)]
33 finalized: bool,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub enum ProjectRuleResult {
39 Checked(RuleResult),
41 Exempt(String),
43 Error(String),
45 Fixed(FixResult),
47 DryRunFix(Vec<RuleViolation>),
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Exemption {
54 pub project: String,
55 pub rule: String,
56 pub reason: Option<String>,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct ComplianceSummary {
62 pub total_projects: usize,
64 pub passing_projects: usize,
66 pub failing_projects: usize,
68 pub total_checks: usize,
70 pub passed_checks: usize,
72 pub failed_checks: usize,
74 pub total_violations: usize,
76 pub violations_by_severity: HashMap<String, usize>,
78 pub fixable_violations: usize,
80 pub pass_rate: f64,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct Violation {
87 pub project: String,
88 pub rule: String,
89 pub code: String,
90 pub message: String,
91 pub severity: ViolationSeverity,
92 pub location: Option<String>,
93 pub fixable: bool,
94}
95
96#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
98pub enum ViolationSeverity {
99 Info,
100 Warning,
101 Error,
102 Critical,
103}
104
105impl From<ViolationLevel> for ViolationSeverity {
106 fn from(level: ViolationLevel) -> Self {
107 match level {
108 ViolationLevel::Info => ViolationSeverity::Info,
109 ViolationLevel::Warning => ViolationSeverity::Warning,
110 ViolationLevel::Error => ViolationSeverity::Error,
111 ViolationLevel::Critical => ViolationSeverity::Critical,
112 }
113 }
114}
115
116impl ComplyReport {
117 pub fn new() -> Self {
119 Self {
120 results: HashMap::new(),
121 exemptions: Vec::new(),
122 errors: Vec::new(),
123 summary: ComplianceSummary::default(),
124 finalized: false,
125 }
126 }
127
128 pub fn add_result(&mut self, project: &str, rule: &str, result: RuleResult) {
130 self.results
131 .entry(project.to_string())
132 .or_default()
133 .insert(rule.to_string(), ProjectRuleResult::Checked(result));
134 }
135
136 pub fn add_exemption(&mut self, project: &str, rule: &str) {
138 self.results
139 .entry(project.to_string())
140 .or_default()
141 .insert(rule.to_string(), ProjectRuleResult::Exempt(rule.to_string()));
142 self.exemptions.push(Exemption {
143 project: project.to_string(),
144 rule: rule.to_string(),
145 reason: None,
146 });
147 }
148
149 pub fn add_error(&mut self, project: &str, rule: &str, error: String) {
151 self.results
152 .entry(project.to_string())
153 .or_default()
154 .insert(rule.to_string(), ProjectRuleResult::Error(error));
155 }
156
157 pub fn add_global_error(&mut self, error: String) {
159 self.errors.push(error);
160 }
161
162 pub fn add_fix_result(&mut self, project: &str, rule: &str, result: FixResult) {
164 self.results
165 .entry(project.to_string())
166 .or_default()
167 .insert(rule.to_string(), ProjectRuleResult::Fixed(result));
168 }
169
170 pub fn add_dry_run_fix(&mut self, project: &str, rule: &str, violations: &[RuleViolation]) {
172 self.results
173 .entry(project.to_string())
174 .or_default()
175 .insert(rule.to_string(), ProjectRuleResult::DryRunFix(violations.to_vec()));
176 }
177
178 pub fn finalize(&mut self) {
180 if self.finalized {
181 return;
182 }
183
184 let mut total_projects = 0;
185 let mut passing_projects = 0;
186 let mut total_checks = 0;
187 let mut passed_checks = 0;
188 let mut failed_checks = 0;
189 let mut total_violations = 0;
190 let mut fixable_violations = 0;
191 let mut violations_by_severity: HashMap<String, usize> = HashMap::new();
192
193 for rules in self.results.values() {
194 total_projects += 1;
195 let mut project_passed = true;
196
197 for result in rules.values() {
198 total_checks += 1;
199
200 match result {
201 ProjectRuleResult::Checked(r) => {
202 if r.passed {
203 passed_checks += 1;
204 } else {
205 failed_checks += 1;
206 project_passed = false;
207
208 total_violations += r.violations.len();
209 fixable_violations += r.violations.iter().filter(|v| v.fixable).count();
210 for v in &r.violations {
211 *violations_by_severity
212 .entry(format!("{}", v.severity))
213 .or_default() += 1;
214 }
215 }
216 }
217 ProjectRuleResult::Exempt(_) => {
218 passed_checks += 1;
219 }
220 ProjectRuleResult::Error(_) => {
221 failed_checks += 1;
222 project_passed = false;
223 }
224 ProjectRuleResult::Fixed(r) => {
225 if r.success {
226 passed_checks += 1;
227 } else {
228 failed_checks += 1;
229 project_passed = false;
230 }
231 }
232 ProjectRuleResult::DryRunFix(violations) => {
233 failed_checks += 1;
234 project_passed = false;
235 total_violations += violations.len();
236 for v in violations {
237 if v.fixable {
238 fixable_violations += 1;
239 }
240 }
241 }
242 }
243 }
244
245 if project_passed {
246 passing_projects += 1;
247 }
248 }
249
250 let pass_rate = if total_checks > 0 {
251 (passed_checks as f64 / total_checks as f64) * 100.0
252 } else {
253 100.0
254 };
255
256 self.summary = ComplianceSummary {
257 total_projects,
258 passing_projects,
259 failing_projects: total_projects - passing_projects,
260 total_checks,
261 passed_checks,
262 failed_checks,
263 total_violations,
264 violations_by_severity,
265 fixable_violations,
266 pass_rate,
267 };
268
269 self.finalized = true;
270 }
271
272 pub fn violations(&self) -> Vec<Violation> {
274 let mut violations = Vec::new();
275
276 for (project, rules) in &self.results {
277 for (rule, result) in rules {
278 if let ProjectRuleResult::Checked(r) = result {
279 for v in &r.violations {
280 violations.push(Violation {
281 project: project.clone(),
282 rule: rule.clone(),
283 code: v.code.clone(),
284 message: v.message.clone(),
285 severity: v.severity.into(),
286 location: v.location.clone(),
287 fixable: v.fixable,
288 });
289 }
290 }
291 }
292 }
293
294 violations
295 }
296
297 pub fn is_compliant(&self) -> bool {
299 self.summary.failing_projects == 0 && self.errors.is_empty()
300 }
301
302 pub fn format_text(&self) -> String {
307 let mut out = String::new();
308
309 writeln!(out, "STACK COMPLIANCE REPORT").ok();
310 writeln!(out, "=======================\n").ok();
311
312 writeln!(
314 out,
315 "Projects: {}/{} passing ({:.1}%)",
316 self.summary.passing_projects, self.summary.total_projects, self.summary.pass_rate
317 )
318 .ok();
319 writeln!(out, "Violations: {}", self.summary.total_violations).ok();
320 if self.summary.fixable_violations > 0 {
321 writeln!(
322 out,
323 "Fixable: {} ({:.1}%)",
324 self.summary.fixable_violations,
325 (self.summary.fixable_violations as f64 / self.summary.total_violations as f64)
326 * 100.0
327 )
328 .ok();
329 }
330 writeln!(out).ok();
331
332 for (project, rules) in &self.results {
334 let passed = rules.values().all(|r| {
335 matches!(r, ProjectRuleResult::Checked(r) if r.passed)
336 || matches!(r, ProjectRuleResult::Exempt(_))
337 });
338
339 let status = if passed { "PASS" } else { "FAIL" };
340 writeln!(out, "{} {} {}", project, ".".repeat(40 - project.len().min(39)), status).ok();
341
342 for (rule, result) in rules {
343 match result {
344 ProjectRuleResult::Checked(r) if !r.passed => {
345 for v in &r.violations {
346 let _ = writeln!(out, " [{:?}] {}: {}", v.severity, v.code, v.message);
347 if let Some(loc) = &v.location {
348 let _ = writeln!(out, " at {}", loc);
349 }
350 }
351 }
352 ProjectRuleResult::Checked(_) => {}
353 ProjectRuleResult::Exempt(reason) => {
354 writeln!(out, " [EXEMPT] {} - {}", rule, reason).ok();
355 }
356 ProjectRuleResult::Error(e) => {
357 writeln!(out, " [ERROR] {} - {}", rule, e).ok();
358 }
359 ProjectRuleResult::Fixed(r) => {
360 writeln!(out, " [FIXED] {} fixes applied", r.fixed_count).ok();
361 }
362 ProjectRuleResult::DryRunFix(violations) => {
363 writeln!(out, " [DRY-RUN] {} violations would be fixed", violations.len())
364 .ok();
365 }
366 }
367 }
368 }
369
370 if !self.errors.is_empty() {
372 writeln!(out, "\nGlobal Errors:").ok();
373 for e in &self.errors {
374 writeln!(out, " - {}", e).ok();
375 }
376 }
377
378 out
379 }
380
381 pub fn format_json(&self) -> String {
383 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
384 }
385
386 pub fn format_markdown(&self) -> String {
388 let mut out = String::new();
389
390 writeln!(out, "# Stack Compliance Report\n").ok();
391
392 writeln!(out, "## Summary\n").ok();
393 writeln!(out, "| Metric | Value |").ok();
394 writeln!(out, "|--------|-------|").ok();
395 writeln!(
396 out,
397 "| Projects Passing | {}/{} ({:.1}%) |",
398 self.summary.passing_projects, self.summary.total_projects, self.summary.pass_rate
399 )
400 .ok();
401 writeln!(out, "| Total Violations | {} |", self.summary.total_violations).ok();
402 writeln!(out, "| Fixable Violations | {} |", self.summary.fixable_violations).ok();
403 writeln!(out).ok();
404
405 writeln!(out, "## Results by Project\n").ok();
406
407 for (project, rules) in &self.results {
408 let passed =
409 rules.values().all(|r| matches!(r, ProjectRuleResult::Checked(r) if r.passed));
410 let emoji = if passed { "✅" } else { "❌" };
411
412 writeln!(out, "### {} {}\n", emoji, project).ok();
413
414 for (rule, result) in rules {
415 match result {
416 ProjectRuleResult::Checked(r) => {
417 if r.passed {
418 writeln!(out, "- ✅ **{}**: Passed", rule).ok();
419 } else {
420 writeln!(out, "- ❌ **{}**: {} violations", rule, r.violations.len())
421 .ok();
422 for v in &r.violations {
423 writeln!(out, " - `{}`: {}", v.code, v.message).ok();
424 }
425 }
426 }
427 ProjectRuleResult::Exempt(reason) => {
428 writeln!(out, "- ⏭️ **{}**: Exempt - {}", rule, reason).ok();
429 }
430 ProjectRuleResult::Error(e) => {
431 writeln!(out, "- ⚠️ **{}**: Error - {}", rule, e).ok();
432 }
433 _ => {}
434 }
435 }
436 writeln!(out).ok();
437 }
438
439 out
440 }
441
442 pub fn format(&self, format: ComplyReportFormat) -> String {
444 match format {
445 ComplyReportFormat::Text => self.format_text(),
446 ComplyReportFormat::Json => self.format_json(),
447 ComplyReportFormat::Markdown => self.format_markdown(),
448 ComplyReportFormat::Html => self.format_html(),
449 }
450 }
451
452 pub fn format_html(&self) -> String {
454 let mut out = String::new();
455
456 writeln!(
457 out,
458 r"<!DOCTYPE html>
459<html>
460<head>
461 <title>Stack Compliance Report</title>
462 <style>
463 body {{ font-family: Roboto, sans-serif; margin: 40px; }}
464 .pass {{ color: #34A853; }}
465 .fail {{ color: #EA4335; }}
466 .warn {{ color: #FBBC04; }}
467 table {{ border-collapse: collapse; width: 100%; }}
468 th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
469 th {{ background-color: #6750A4; color: white; }}
470 tr:nth-child(even) {{ background-color: #f2f2f2; }}
471 </style>
472</head>
473<body>
474 <h1>Stack Compliance Report</h1>
475
476 <h2>Summary</h2>
477 <table>
478 <tr><th>Metric</th><th>Value</th></tr>
479 <tr><td>Projects</td><td>{}/{} ({:.1}%)</td></tr>
480 <tr><td>Total Violations</td><td>{}</td></tr>
481 <tr><td>Fixable</td><td>{}</td></tr>
482 </table>
483",
484 self.summary.passing_projects,
485 self.summary.total_projects,
486 self.summary.pass_rate,
487 self.summary.total_violations,
488 self.summary.fixable_violations
489 )
490 .ok();
491
492 writeln!(out, " <h2>Results</h2>").ok();
493 writeln!(out, " <table>").ok();
494 writeln!(out, " <tr><th>Project</th><th>Status</th><th>Violations</th></tr>").ok();
495
496 for (project, rules) in &self.results {
497 let passed =
498 rules.values().all(|r| matches!(r, ProjectRuleResult::Checked(r) if r.passed));
499 let status_class = if passed { "pass" } else { "fail" };
500 let status = if passed { "PASS" } else { "FAIL" };
501
502 let violation_count: usize = rules
503 .values()
504 .filter_map(|r| match r {
505 ProjectRuleResult::Checked(r) => Some(r.violations.len()),
506 _ => None,
507 })
508 .sum();
509
510 writeln!(
511 out,
512 " <tr><td>{}</td><td class=\"{}\">{}</td><td>{}</td></tr>",
513 project, status_class, status, violation_count
514 )
515 .ok();
516 }
517
518 writeln!(out, " </table>").ok();
519 writeln!(out, "</body></html>").ok();
520
521 out
522 }
523}
524
525impl Default for ComplyReport {
526 fn default() -> Self {
527 Self::new()
528 }
529}
530
531#[cfg(test)]
532#[path = "report_tests.rs"]
533mod tests;