pipa_core/engine/contracts/
runner.rs

1//! Note: Before moving data, we validate profile connectivity.
2//! - If source profile is invalid → abort validation.
3//! - If destination/quarantine profile is invalid → skip movement and log error.
4//! This prevents wasted work and clearer operator feedback.
5use crate::connectors::fetch::fetch_data_from_source; // fetch raw bytes from source connector
6use crate::contracts::load_contract_for_file; // load TOML contract into SchemaContracts
7use crate::engine::log_action; // audit logging
8use crate::engine::validation::execute_validation; // run validators against data
9use crate::logging::error::{ValidationError, ValidationResult};
10use crate::logging::schema::{Executor, RuleResult};
11use crate::logging::{AuditLogEntry, writer::log_and_print};
12use crate::movement::FileMovement; // handles writing success/quarantine data
13use crate::profiles::{Profiles, load_profiles}; // profile management
14use chrono::Utc;
15use std::path::Path as StdPath;
16
17/// Outcome of running a contract validation
18pub struct ValidationOutcome {
19    pub passed: bool,             // true if no rules failed
20    pub pass_count: usize,        // number of passing rules
21    pub fail_count: usize,        // number of failing rules
22    pub results: Vec<RuleResult>, // detailed results per rule
23}
24
25/// Run a contract validation end-to-end:
26/// - Load contract + profiles
27/// - Fetch data from source
28/// - Execute validations
29/// - Move data to destination or quarantine (only if profiles are valid)
30/// - Log all actions
31pub async fn run_contract_validation(
32    contract_name: &str,
33    executor: &Executor,
34    log_to_console: bool,
35) -> ValidationResult<(ValidationOutcome, String)> {
36    // --- Contract existence check ---
37    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    // --- Load contract + profiles ---
46    let contracts = load_contract_for_file(StdPath::new(&contract_path));
47    let profiles: Profiles = load_profiles()?;
48
49    // --- Validate source config ---
50    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    // --- Start log ---
60    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    // --- Fetch data ---
72    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    // --- Determine file extension ---
82    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    // --- Execute validations ---
89    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    // --- Load DataFrame for movement ---
95    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    // --- Validate profile connectivity before movement ---
100    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    // --- Movement logic ---
115    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    // --- Completion log ---
249    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}