use crate::cook::workflow::variables::VariableStore;
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::path::PathBuf;
type RestoredVariables = (
HashMap<String, String>,
HashMap<String, String>,
HashMap<String, String>,
);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariableCheckpointState {
pub global_variables: HashMap<String, Value>,
pub phase_variables: HashMap<String, HashMap<String, Value>>,
pub computed_cache: HashMap<String, CachedValue>,
pub environment_snapshot: EnvironmentSnapshot,
pub interpolation_history: Vec<InterpolationRecord>,
pub variable_metadata: VariableMetadata,
pub captured_outputs: HashMap<String, String>,
pub iteration_vars: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedValue {
pub value: Value,
pub computed_at: DateTime<Utc>,
pub cache_key: String,
pub dependencies: Vec<String>,
pub is_expensive: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentSnapshot {
pub variables: HashMap<String, String>,
pub captured_at: DateTime<Utc>,
pub hostname: String,
pub working_directory: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InterpolationRecord {
pub template: String,
pub result: String,
pub interpolated_at: DateTime<Utc>,
pub variable_dependencies: Vec<String>,
pub phase: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariableMetadata {
pub total_variables: usize,
pub computed_variables: usize,
pub total_interpolations: usize,
pub checkpoint_version: String,
}
#[derive(Debug, Clone)]
pub struct EnvironmentCompatibility {
pub missing_variables: HashMap<String, String>,
pub changed_variables: HashMap<String, (String, String)>,
pub new_variables: HashMap<String, String>,
pub is_compatible: bool,
}
impl Default for EnvironmentCompatibility {
fn default() -> Self {
Self::new()
}
}
impl EnvironmentCompatibility {
pub fn new() -> Self {
Self {
missing_variables: HashMap::new(),
changed_variables: HashMap::new(),
new_variables: HashMap::new(),
is_compatible: true,
}
}
pub fn add_missing_variable(&mut self, key: String, value: String) {
self.missing_variables.insert(key, value);
self.is_compatible = false; }
pub fn add_changed_variable(&mut self, key: String, old_value: String, new_value: String) {
self.changed_variables.insert(key, (old_value, new_value));
self.is_compatible = false; }
pub fn add_new_variable(&mut self, key: String, value: String) {
self.new_variables.insert(key, value);
}
pub fn has_critical_changes(&self) -> bool {
!self.missing_variables.is_empty() || !self.changed_variables.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct InterpolationTestResults {
pub tests: Vec<InterpolationTest>,
pub total_tests: usize,
pub passed_tests: usize,
pub failed_tests: usize,
pub test_duration: std::time::Duration,
}
impl Default for InterpolationTestResults {
fn default() -> Self {
Self::new()
}
}
impl InterpolationTestResults {
pub fn new() -> Self {
Self {
tests: Vec::new(),
total_tests: 0,
passed_tests: 0,
failed_tests: 0,
test_duration: std::time::Duration::default(),
}
}
pub fn add_test(&mut self, test: InterpolationTest) {
self.total_tests += 1;
if test.matches {
self.passed_tests += 1;
} else {
self.failed_tests += 1;
}
self.tests.push(test);
}
pub fn all_passed(&self) -> bool {
self.failed_tests == 0
}
}
#[derive(Debug, Clone)]
pub struct InterpolationTest {
pub template: String,
pub original_result: String,
pub current_result: String,
pub matches: bool,
pub interpolated_at: DateTime<Utc>,
}
pub struct VariableResumeManager {}
impl Default for VariableResumeManager {
fn default() -> Self {
Self::new()
}
}
impl VariableResumeManager {
pub fn new() -> Self {
Self {}
}
pub fn create_checkpoint(
&self,
variables: &HashMap<String, String>,
captured_outputs: &HashMap<String, String>,
iteration_vars: &HashMap<String, String>,
_variable_store: &VariableStore,
) -> Result<VariableCheckpointState> {
let mut global_variables = HashMap::new();
for (key, value) in variables {
global_variables.insert(key.clone(), Value::String(value.clone()));
}
for (key, value) in captured_outputs {
global_variables.insert(key.clone(), Value::String(value.clone()));
}
let environment_snapshot = EnvironmentSnapshot {
variables: std::env::vars().collect(),
captured_at: Utc::now(),
hostname: hostname::get()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
working_directory: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
};
let variable_metadata = VariableMetadata {
total_variables: global_variables.len(),
computed_variables: 0,
total_interpolations: 0,
checkpoint_version: "1.0.0".to_string(),
};
Ok(VariableCheckpointState {
global_variables,
phase_variables: HashMap::new(),
computed_cache: HashMap::new(),
environment_snapshot,
interpolation_history: Vec::new(),
variable_metadata,
captured_outputs: captured_outputs.clone(),
iteration_vars: iteration_vars.clone(),
})
}
pub fn restore_from_checkpoint(
&self,
state: &VariableCheckpointState,
) -> Result<RestoredVariables> {
let mut variables = HashMap::new();
let mut captured_outputs = HashMap::new();
let iteration_vars = state.iteration_vars.clone();
for (key, value) in &state.global_variables {
if let Value::String(s) = value {
variables.insert(key.clone(), s.clone());
} else {
variables.insert(key.clone(), value.to_string());
}
}
for (key, value) in &state.captured_outputs {
captured_outputs.insert(key.clone(), value.clone());
}
Ok((variables, captured_outputs, iteration_vars))
}
pub fn validate_environment(
&self,
saved_snapshot: &EnvironmentSnapshot,
) -> Result<EnvironmentCompatibility> {
let mut compatibility = EnvironmentCompatibility::new();
let current_env: HashMap<String, String> = std::env::vars().collect();
for (key, saved_value) in &saved_snapshot.variables {
if key.starts_with("PRODIGY_") || key.starts_with("_") {
continue;
}
match current_env.get(key) {
Some(current_value) if current_value != saved_value => {
compatibility.add_changed_variable(
key.clone(),
saved_value.clone(),
current_value.clone(),
);
}
None => {
if !key.starts_with("RUST_") && !key.contains("TEMP") && !key.contains("TMP") {
compatibility.add_missing_variable(key.clone(), saved_value.clone());
}
}
_ => {} }
}
for (key, current_value) in ¤t_env {
if !saved_snapshot.variables.contains_key(key) && !key.starts_with("PRODIGY_") {
compatibility.add_new_variable(key.clone(), current_value.clone());
}
}
compatibility.is_compatible = !compatibility.has_critical_changes();
Ok(compatibility)
}
pub fn recalculate_mapreduce_variables(
&self,
total_items: usize,
successful_items: usize,
failed_items: usize,
) -> HashMap<String, String> {
let mut variables = HashMap::new();
variables.insert("map.total".to_string(), total_items.to_string());
variables.insert("map.successful".to_string(), successful_items.to_string());
variables.insert("map.failed".to_string(), failed_items.to_string());
variables.insert(
"map.completed".to_string(),
(successful_items + failed_items).to_string(),
);
let success_rate = if total_items > 0 {
(successful_items as f64 / total_items as f64) * 100.0
} else {
0.0
};
variables.insert(
"map.success_rate".to_string(),
format!("{:.2}", success_rate),
);
variables
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_environment_compatibility() {
let mut compatibility = EnvironmentCompatibility::new();
compatibility.add_missing_variable("API_KEY".to_string(), "secret".to_string());
compatibility.add_changed_variable(
"ENV".to_string(),
"production".to_string(),
"development".to_string(),
);
assert!(compatibility.has_critical_changes());
assert!(!compatibility.is_compatible);
}
#[test]
fn test_mapreduce_variable_recalculation() {
let manager = VariableResumeManager::new();
let vars = manager.recalculate_mapreduce_variables(10, 7, 3);
assert_eq!(vars.get("map.total").unwrap(), "10");
assert_eq!(vars.get("map.successful").unwrap(), "7");
assert_eq!(vars.get("map.failed").unwrap(), "3");
assert_eq!(vars.get("map.completed").unwrap(), "10");
assert_eq!(vars.get("map.success_rate").unwrap(), "70.00");
}
#[test]
fn test_interpolation_test_results() {
let mut results = InterpolationTestResults::new();
results.add_test(InterpolationTest {
template: "${item}".to_string(),
original_result: "test.txt".to_string(),
current_result: "test.txt".to_string(),
matches: true,
interpolated_at: Utc::now(),
});
results.add_test(InterpolationTest {
template: "${map.total}".to_string(),
original_result: "10".to_string(),
current_result: "0".to_string(),
matches: false,
interpolated_at: Utc::now(),
});
assert_eq!(results.total_tests, 2);
assert_eq!(results.passed_tests, 1);
assert_eq!(results.failed_tests, 1);
assert!(!results.all_passed());
}
}