use crate::error::DispatchError;
use crate::ir::WhenGuard;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StepOutcome {
Success,
Failure,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StepIdentity {
pub name: String,
pub produces: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct GuardState {
pub artifacts: HashMap<String, String>,
pub outcomes: HashMap<String, StepOutcome>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum ConditionalDecision {
Run,
Skip,
Repair { target: StepIdentity },
}
pub const DEFAULT_MAX_REPAIR_ATTEMPTS: usize = 1;
fn read_flag(artifacts: &HashMap<String, String>, target: &str) -> bool {
let mut parts = target.split('.');
let Some(artifact_name) = parts.next() else {
return false;
};
let Some(raw) = artifacts.get(artifact_name) else {
return false;
};
let Ok(mut cur) = serde_json::from_str::<serde_json::Value>(raw) else {
return false;
};
for key in parts {
let serde_json::Value::Object(map) = &cur else {
return false;
};
match map.get(key) {
Some(v) => cur = v.clone(),
None => return false,
}
}
matches!(cur, serde_json::Value::Bool(true))
}
pub fn evaluate_guard(when: &WhenGuard, state: &GuardState) -> Result<bool, DispatchError> {
match when.guard.as_str() {
"on_failure" => Ok(state.outcomes.get(&when.target) == Some(&StepOutcome::Failure)),
"on_flag" => Ok(read_flag(&state.artifacts, &when.target)),
"on_missing" => Ok(!state.artifacts.contains_key(&when.target)),
other => Err(DispatchError::new(format!(
"unknown guard '{other}' (closed vocabulary: on_failure, on_flag, on_missing)"
))),
}
}
pub fn repair_fold_target(
when: &WhenGuard,
consumes: &[String],
preceding_step: Option<&StepIdentity>,
) -> Option<StepIdentity> {
let preceding = preceding_step?;
if when.guard != "on_failure" || when.target != preceding.name {
return None;
}
let produces = preceding.produces.as_ref()?;
if !consumes.iter().any(|c| c == produces) {
return None;
}
Some(preceding.clone())
}
pub fn classify_conditional_step(
when: &WhenGuard,
consumes: &[String],
preceding_step: Option<&StepIdentity>,
state: &GuardState,
) -> Result<ConditionalDecision, DispatchError> {
if let Some(target) = repair_fold_target(when, consumes, preceding_step) {
return Ok(
if state.outcomes.get(&target.name) == Some(&StepOutcome::Failure) {
ConditionalDecision::Repair { target }
} else {
ConditionalDecision::Skip
},
);
}
Ok(if evaluate_guard(when, state)? {
ConditionalDecision::Run
} else {
ConditionalDecision::Skip
})
}
pub fn run_repair_loop(
max_attempts: usize,
mut attempt_failed: impl FnMut(usize) -> bool,
) -> (bool, usize) {
for attempt in 1..=max_attempts {
if !attempt_failed(attempt) {
return (true, attempt);
}
}
(false, max_attempts)
}