1use oparry_core::{Report, ValidationResult};
4use colored::Colorize;
5
6pub trait OutputFormatter {
8 fn format_result(&self, result: &ValidationResult) -> String;
10 fn format_report(&self, report: &Report) -> String;
12}
13
14pub struct HumanFormatter {
16 show_paths: bool,
17 use_colors: bool,
18}
19
20impl HumanFormatter {
21 pub fn new(show_paths: bool, use_colors: bool) -> Self {
23 Self {
24 show_paths,
25 use_colors,
26 }
27 }
28}
29
30impl OutputFormatter for HumanFormatter {
31 fn format_result(&self, result: &ValidationResult) -> String {
32 let mut output = String::new();
33
34 for issue in &result.issues {
35 let icon = match issue.level {
36 oparry_core::IssueLevel::Error => {
37 if self.use_colors {
38 "✗".red()
39 } else {
40 "✗".normal()
41 }
42 }
43 oparry_core::IssueLevel::Warning => {
44 if self.use_colors {
45 "⚠".yellow()
46 } else {
47 "⚠".normal()
48 }
49 }
50 oparry_core::IssueLevel::Note => {
51 if self.use_colors {
52 "ℹ".blue()
53 } else {
54 "ℹ".normal()
55 }
56 }
57 };
58
59 let level_str = match issue.level {
60 oparry_core::IssueLevel::Error => "error",
61 oparry_core::IssueLevel::Warning => "warning",
62 oparry_core::IssueLevel::Note => "note",
63 };
64
65 output.push_str(&format!("{} ", icon));
66
67 if let Some(ref file) = issue.file {
68 output.push_str(&format!("{} ", file));
69 }
70
71 output.push_str(&format!("{}\n", level_str));
72
73 output.push_str(&format!(" --> {}\n", issue.message));
74
75 if let Some(ref suggestion) = issue.suggestion {
76 output.push_str(&format!(" suggestion: {}\n", suggestion));
77 }
78
79 output.push('\n');
80 }
81
82 let errors = result.error_count();
84 let warnings = result.warning_count();
85
86 if errors > 0 || warnings > 0 {
87 output.push_str(&format!(
88 "{} {}, {} {}\n",
89 errors.to_string().red().bold(),
90 if errors == 1 { "error" } else { "errors" },
91 warnings.to_string().yellow().bold(),
92 if warnings == 1 { "warning" } else { "warnings" }
93 ));
94 } else {
95 output.push_str(&format!("{} No issues found\n", "✓".green().bold()));
96 }
97
98 output
99 }
100
101 fn format_report(&self, report: &Report) -> String {
102 self.format_result(&report.result)
103 }
104}
105
106pub struct JsonFormatter;
108
109impl JsonFormatter {
110 pub fn new() -> Self {
112 Self
113 }
114}
115
116impl Default for JsonFormatter {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122impl OutputFormatter for JsonFormatter {
123 fn format_result(&self, result: &ValidationResult) -> String {
124 serde_json::to_string_pretty(result).unwrap_or_else(|_| "{}".to_string())
125 }
126
127 fn format_report(&self, report: &Report) -> String {
128 serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string())
129 }
130}
131
132pub struct SarifFormatter;
134
135impl SarifFormatter {
136 pub fn new() -> Self {
138 Self
139 }
140}
141
142impl Default for SarifFormatter {
143 fn default() -> Self {
144 Self::new()
145 }
146}
147
148impl OutputFormatter for SarifFormatter {
149 fn format_result(&self, result: &ValidationResult) -> String {
150 let report = Report::new(result.clone());
151 self.format_report(&report)
152 }
153
154 fn format_report(&self, report: &Report) -> String {
155 match report.to_sarif() {
156 Ok(sarif) => serde_json::to_string_pretty(&sarif).unwrap_or_else(|_| "{}".to_string()),
157 Err(e) => format!(r#"{{"error": "{}"}}"#, e),
158 }
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use oparry_core::{Issue, IssueLevel};
166
167 #[test]
168 fn test_json_formatter() {
169 let formatter = JsonFormatter::new();
170 let mut result = ValidationResult::new();
171 result.add_issue(Issue::error("test", "test message"));
172
173 let output = formatter.format_result(&result);
174 assert!(output.contains("\"error\""));
175 assert!(output.contains("test message"));
176 }
177}