cherry_evm_validate/
issues_collector.rs1use chrono::Local;
2use std::collections::HashMap;
3use std::fmt::{self, Display};
4use std::fs::File;
5use std::io::Write;
6use std::path::Path;
7
8#[derive(Debug, Clone)]
9pub struct IssueCollectorConfig {
10 pub console_output: bool,
11 pub emit_report: bool,
12 pub report_path: String,
13 pub stop_on_issue: bool,
14 pub report_format: ReportFormat,
15 pub current_context: DataContext,
16}
17
18#[derive(Debug, Clone)]
19pub enum ReportFormat {
20 Text,
21 Json,
22}
23
24impl Default for IssueCollectorConfig {
25 fn default() -> Self {
26 Self {
27 console_output: true,
28 emit_report: true,
29 report_path: "data_issues_report.txt".to_string(),
30 stop_on_issue: false,
31 report_format: ReportFormat::Text,
32 current_context: DataContext::default(),
33 }
34 }
35}
36
37#[derive(Debug)]
38pub struct IssueCollector {
39 issues: Vec<Issue>,
40 config: IssueCollectorConfig,
41}
42
43#[derive(Debug, Clone)]
44pub struct Issue {
45 context: DataContext,
46 issue: String,
47}
48
49#[derive(Debug, Clone)]
52pub struct DataContext {
53 table: String,
54 row: String,
55}
56
57impl Display for DataContext {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 write!(f, "In the {} table, row with {}.", self.table, self.row)
60 }
61}
62
63impl Default for DataContext {
64 fn default() -> Self {
65 Self {
66 table: "Undefined".to_string(),
67 row: "Undefined".to_string(),
68 }
69 }
70}
71
72impl DataContext {
73 pub fn new(table: String, row: String) -> Self {
74 Self { table, row }
75 }
76}
77
78impl IssueCollector {
79 pub fn new(config: IssueCollectorConfig) -> Self {
80 Self {
81 issues: Vec::new(),
82 config,
83 }
84 }
85
86 pub fn with_default_config() -> Self {
87 Self::new(IssueCollectorConfig::default())
88 }
89
90 pub fn set_context(&mut self, context: DataContext) {
91 self.config.current_context = context;
92 }
93
94 pub fn report<D>(&mut self, issue: &str, default: D) -> D {
96 let ctx = self.config.current_context.clone();
97 if self.config.console_output {
98 eprintln!("Data issue found: {} {}", ctx, issue);
99 }
100
101 if self.config.stop_on_issue {
102 eprintln!("Validation failed. Configs have stop_on_issue set to true.");
103 std::process::exit(1);
104 }
105
106 self.issues.push(Issue {
107 context: ctx,
108 issue: issue.to_string(),
109 });
110
111 default
112 }
113
114 pub fn report_with_context<D>(&mut self, issue: &str, context: DataContext, default: D) -> D {
115 if self.config.console_output {
116 eprintln!("Data issue found: {} {}", context, issue);
117 }
118
119 if self.config.stop_on_issue {
120 std::process::exit(1);
121 }
122
123 self.issues.push(Issue {
124 context,
125 issue: issue.to_string(),
126 });
127
128 default
129 }
130
131 pub fn write_report(&self) -> std::io::Result<()> {
133 if !self.config.emit_report || self.issues.is_empty() {
134 return Ok(());
135 }
136
137 if let Some(parent) = Path::new(&self.config.report_path).parent() {
139 if !parent.exists() {
140 std::fs::create_dir_all(parent)?;
141 }
142 }
143
144 match self.config.report_format {
145 ReportFormat::Text => self.write_text_report(&self.config.report_path),
146 ReportFormat::Json => self.write_json_report(&self.config.report_path),
147 }
148 }
149
150 fn write_text_report(&self, path: &str) -> std::io::Result<()> {
151 let mut file = File::create(path)?;
152
153 writeln!(file, "Data Validation Issues Report - {}", Local::now())?;
154 writeln!(file, "=============================================")?;
155
156 for (i, issue) in self.issues.iter().enumerate() {
157 writeln!(
158 file,
159 "#{}: Context: {} Issue: {}",
160 i + 1,
161 issue.context,
162 issue.issue
163 )?;
164 }
165
166 writeln!(file, "\nTotal issues: {}", self.issues.len())?;
167
168 Ok(())
169 }
170
171 fn write_json_report(&self, path: &str) -> std::io::Result<()> {
172 let report_time = Local::now().to_string();
173 let mut issues = Vec::new();
174
175 for issue in &self.issues {
176 let mut entry = HashMap::new();
177 entry.insert("context", issue.context.to_string());
178 entry.insert("issue", issue.issue.clone());
179 issues.push(entry);
180 }
181
182 let report = HashMap::from([
183 ("timestamp", report_time),
184 ("total_issues", self.issues.len().to_string()),
185 ]);
186
187 let mut file = File::create(path)?;
188 let json = serde_json::json!({
189 "report_info": report,
190 "issues": issues
191 });
192
193 file.write_all(serde_json::to_string_pretty(&json)?.as_bytes())?;
194
195 Ok(())
196 }
197}
198
199impl Drop for IssueCollector {
201 fn drop(&mut self) {
202 if let Err(e) = self.write_report() {
203 eprintln!("Failed to write issue report: {}", e);
204 }
205 }
206}