cherry_evm_validate/
issues_collector.rs

1use 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/// Context of the data that is being processed.
50/// It is used to losely identify the source of the issue.
51#[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    // Report an issue and optionally stop execution
95    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    // Write report at the end of execution
132    pub fn write_report(&self) -> std::io::Result<()> {
133        if !self.config.emit_report || self.issues.is_empty() {
134            return Ok(());
135        }
136
137        // Create directory if it doesn't exist
138        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
199// Implementation for Drop to automatically write the report when the collector goes out of scope
200impl 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}