pipa_core/engine/contracts/
runner.rs1use crate::connectors::fetch::fetch_data_from_source; use crate::contracts::load_contract_for_file; use crate::engine::log_action; use crate::engine::validation::execute_validation; use crate::logging::error::{ValidationError, ValidationResult};
10use crate::logging::schema::{Executor, RuleResult};
11use crate::logging::{AuditLogEntry, AuditLogger};
12use crate::movement::FileMovement; use crate::profiles::{Profiles, load_profiles}; use chrono::Utc;
15use std::path::Path as StdPath;
16
17pub struct ValidationOutcome {
19 pub passed: bool, pub pass_count: usize, pub fail_count: usize, pub results: Vec<RuleResult>, }
24
25pub async fn run_contract_validation<L: AuditLogger>(
38 logger: &L,
39 contract_name: &str,
40 executor: &Executor,
41 log_to_console: bool,
42) -> ValidationResult<(ValidationOutcome, String)> {
43 let contract_path = format!("contracts/{}.toml", contract_name);
45 if !StdPath::new(&contract_path).exists() {
46 return Err(ValidationError::Other(format!(
47 "Contract '{}' not found",
48 contract_name
49 )));
50 }
51
52 let contracts = load_contract_for_file(StdPath::new(&contract_path));
54 let profiles: Profiles = load_profiles()?;
55
56 let source = contracts
58 .source
59 .as_ref()
60 .ok_or_else(|| ValidationError::Other("Contract missing source".to_string()))?;
61 let location = source
62 .location
63 .as_ref()
64 .ok_or_else(|| ValidationError::Other("Source missing location".to_string()))?;
65
66 let start_message = log_action(
68 logger,
69 "contract_validation_started",
70 None,
71 Some(contract_name),
72 None,
73 None,
74 );
75 if log_to_console {
76 println!("{}", start_message);
77 }
78
79 let data = fetch_data_from_source(source, &profiles).await?;
81 let _ = log_action(
82 logger,
83 "file_read",
84 Some(&format!("bytes={}", data.len())),
85 None,
86 None,
87 Some(location),
88 );
89
90 let extension = source
92 .location
93 .as_ref()
94 .and_then(|loc| StdPath::new(loc).extension().and_then(|s| s.to_str()))
95 .unwrap_or("csv");
96
97 let results = execute_validation(&data, extension, &contracts, executor).await?;
99 let pass_count = results.iter().filter(|r| r.result == "pass").count();
100 let fail_count = results.iter().filter(|r| r.result == "fail").count();
101 let validation_passed = fail_count == 0;
102
103 let original_location = source.location.as_deref().unwrap_or("unknown");
105 let driver = crate::drivers::get_driver(extension)?;
106 let df = driver.load(&data)?;
107
108 let (source_valid, dest_valid, quarantine_valid) = FileMovement::validate_profiles(
110 contracts.source.as_ref(),
111 contracts.destination.as_ref(),
112 contracts.quarantine.as_ref(),
113 &profiles,
114 )
115 .await;
116
117 if !source_valid {
118 return Err(ValidationError::Other(
119 "Source profile connectivity failed".to_string(),
120 ));
121 }
122
123 if validation_passed {
125 if let Some(dest) = &contracts.destination {
126 if dest.r#type != "not_moved" {
127 if !dest_valid {
128 logger.log_and_print(
129 &AuditLogEntry {
130 timestamp: Utc::now().to_rfc3339(),
131 level: "AUDIT",
132 event: "movement_skipped",
133 contract: Some(crate::logging::schema::Contract {
134 name: &contracts.contract.name,
135 version: &contracts.contract.version,
136 }),
137 target: None,
138 results: None,
139 executor: executor.clone(),
140 details: Some("Destination profile connectivity failed"),
141 summary: None,
142 },
143 "⚠️ Skipped movement: destination profile invalid",
144 );
145 } else {
146 match FileMovement::write_success_data(&df, original_location, dest, &profiles)
147 .await
148 {
149 Ok(_) => logger.log_and_print(
150 &AuditLogEntry {
151 timestamp: Utc::now().to_rfc3339(),
152 level: "AUDIT",
153 event: "movement_success",
154 contract: Some(crate::logging::schema::Contract {
155 name: &contracts.contract.name,
156 version: &contracts.contract.version,
157 }),
158 target: None,
159 results: None,
160 executor: executor.clone(),
161 details: Some("Data written to destination"),
162 summary: None,
163 },
164 "✅ Data written to destination",
165 ),
166 Err(e) => logger.log_and_print(
167 &AuditLogEntry {
168 timestamp: Utc::now().to_rfc3339(),
169 level: "AUDIT",
170 event: "movement_error",
171 contract: Some(crate::logging::schema::Contract {
172 name: &contracts.contract.name,
173 version: &contracts.contract.version,
174 }),
175 target: None,
176 results: None,
177 executor: executor.clone(),
178 details: Some(&format!("Failed to write to destination: {}", e)),
179 summary: None,
180 },
181 &format!("❌ Failed to write to destination: {}", e),
182 ),
183 }
184 }
185 }
186 }
187 } else {
188 if let Some(quarantine) = &contracts.quarantine {
189 if quarantine.r#type != "not_moved" {
190 if !quarantine_valid {
191 logger.log_and_print(
192 &AuditLogEntry {
193 timestamp: Utc::now().to_rfc3339(),
194 level: "AUDIT",
195 event: "movement_skipped",
196 contract: Some(crate::logging::schema::Contract {
197 name: &contracts.contract.name,
198 version: &contracts.contract.version,
199 }),
200 target: None,
201 results: None,
202 executor: executor.clone(),
203 details: Some("Quarantine profile connectivity failed"),
204 summary: None,
205 },
206 "⚠️ Skipped movement: quarantine profile invalid",
207 );
208 } else {
209 match FileMovement::write_quarantine_data(
210 &df,
211 original_location,
212 quarantine,
213 &profiles,
214 )
215 .await
216 {
217 Ok(_) => logger.log_and_print(
218 &AuditLogEntry {
219 timestamp: Utc::now().to_rfc3339(),
220 level: "AUDIT",
221 event: "movement_quarantine",
222 contract: Some(crate::logging::schema::Contract {
223 name: &contracts.contract.name,
224 version: &contracts.contract.version,
225 }),
226 target: None,
227 results: None,
228 executor: executor.clone(),
229 details: Some("Data written to quarantine"),
230 summary: None,
231 },
232 "⚠️ Data quarantined",
233 ),
234 Err(e) => logger.log_and_print(
235 &AuditLogEntry {
236 timestamp: Utc::now().to_rfc3339(),
237 level: "AUDIT",
238 event: "movement_error",
239 contract: Some(crate::logging::schema::Contract {
240 name: &contracts.contract.name,
241 version: &contracts.contract.version,
242 }),
243 target: None,
244 results: None,
245 executor: executor.clone(),
246 details: Some(&format!("Failed to write to quarantine: {}", e)),
247 summary: None,
248 },
249 &format!("❌ Failed to write to quarantine: {}", e),
250 ),
251 }
252 }
253 }
254 }
255 }
256
257 let details = format!("pass={}, fail={}", pass_count, fail_count);
259 let message = log_action(
260 logger,
261 "contract_validation_completed",
262 Some(&details),
263 Some(&contracts.contract.name),
264 Some(&contracts.contract.version),
265 None,
266 );
267 if log_to_console {
268 println!("{}", message);
269 }
270
271 Ok((
272 ValidationOutcome {
273 passed: validation_passed,
274 pass_count,
275 fail_count,
276 results,
277 },
278 message,
279 ))
280}