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