use std::collections::{HashMap, HashSet, VecDeque};
use serde_json::{json, Value};
use crate::event::Event;
use crate::node::{StepNode, WorkflowContext};
use crate::recorder::{RunStatus, StepStatus};
use crate::registry::{NodeError, NodeRegistry};
use crate::result::StepResult;
use crate::spec::Branch;
#[derive(Debug, thiserror::Error)]
pub enum CompileError {
#[error(transparent)]
Node(#[from] NodeError),
#[error("branch '{branch_id}' has no ingress node (chain needs a source)")]
NoIngress { branch_id: String },
#[error("branch '{branch_id}' has {count} ingress nodes; day-1 supports one")]
MultipleIngress { branch_id: String, count: usize },
#[error("branch '{branch_id}' edges form a cycle (DAG required)")]
Cycle { branch_id: String },
}
pub(crate) struct CompiledBranch {
steps: Vec<CompiledStep>,
}
struct CompiledStep {
node_id: String,
node_type: String,
node: Box<dyn StepNode>,
}
fn is_material(node_type: &str) -> bool {
node_type.starts_with("execute.") || node_type.starts_with("notify.")
}
fn event_detail(event: &Event) -> Value {
json!({
"event_id": event.id,
"payload": event.payload,
"metadata": event.metadata,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Terminal {
Dropped { node_id: String, reason: String },
Completed,
}
#[derive(Debug, Clone)]
pub struct RunOutcome {
pub steps_run: usize,
pub terminal: Terminal,
pub survivors: Vec<Event>,
}
impl CompiledBranch {
pub(crate) fn compile(branch: &Branch, registry: &NodeRegistry) -> Result<Self, CompileError> {
let order = topo_order(branch)?;
let ingress_nodes: Vec<&crate::spec::Node> = branch
.nodes
.iter()
.filter(|n| registry.is_ingress(&n.node_type))
.collect();
match ingress_nodes.len() {
0 => {
return Err(CompileError::NoIngress {
branch_id: branch.branch_id.clone(),
})
}
1 => {}
n => {
return Err(CompileError::MultipleIngress {
branch_id: branch.branch_id.clone(),
count: n,
})
}
}
let by_id: HashMap<&str, &crate::spec::Node> =
branch.nodes.iter().map(|n| (n.id.as_str(), n)).collect();
let mut steps = Vec::new();
for node_id in order {
let node = by_id[node_id.as_str()];
if registry.is_ingress(&node.node_type) {
continue; }
let built = registry.build_step(&node.node_type, &node.config)?;
steps.push(CompiledStep {
node_id: node.id.clone(),
node_type: node.node_type.clone(),
node: built,
});
}
Ok(Self { steps })
}
pub(crate) async fn run_event(&self, ctx: &WorkflowContext, event: Event) -> RunOutcome {
let trigger = Value::Object(event.payload.clone());
let mut run = ctx.recorder.start(&ctx.trigger_kind, trigger).await;
let mut current = vec![event];
let mut steps_run = 0;
let mut material_steps = 0u32;
for step in &self.steps {
let mut next = Vec::new();
let mut last_drop: Option<(String, Option<String>)> = None;
for ev in ¤t {
match step.node.process(ev, ctx).await {
StepResult::Pass(event) => {
if is_material(&step.node_type) {
run.record_step(
&step.node_id,
&step.node_type,
StepStatus::Ok,
None,
event_detail(&event),
)
.await;
material_steps += 1;
}
next.push(event);
}
StepResult::Drop {
reason,
exit_reason,
} => last_drop = Some((reason, exit_reason)),
StepResult::FanOut(evs) => next.extend(evs),
}
}
steps_run += 1;
if next.is_empty() {
let (reason, exit_reason) = last_drop.unwrap_or_else(|| ("dropped".into(), None));
if step.node_type.starts_with("sink.") || material_steps > 0 {
run.end(RunStatus::Ok, Some("natural")).await;
} else if let Some(code) = exit_reason {
let step_status = if code.starts_with("invalid_") {
StepStatus::Error
} else {
StepStatus::Skipped
};
run.record_step(
&step.node_id,
&step.node_type,
step_status,
Some(&code),
json!({ "reason": reason }),
)
.await;
run.end(
if step_status == StepStatus::Error {
RunStatus::Error
} else {
RunStatus::Skipped
},
Some(&code),
)
.await;
} else {
run.mark_filtered(&step.node_id, &step.node_type, &reason)
.await;
run.end(RunStatus::Skipped, None).await;
}
return RunOutcome {
steps_run,
terminal: Terminal::Dropped {
node_id: step.node_id.clone(),
reason,
},
survivors: Vec::new(),
};
}
current = next;
}
run.end(
if material_steps > 0 {
RunStatus::Ok
} else {
RunStatus::Skipped
},
Some("natural"),
)
.await;
RunOutcome {
steps_run,
terminal: Terminal::Completed,
survivors: current,
}
}
}
fn topo_order(branch: &Branch) -> Result<Vec<String>, CompileError> {
let ids: Vec<&str> = branch.nodes.iter().map(|n| n.id.as_str()).collect();
let id_set: HashSet<&str> = ids.iter().copied().collect();
let mut indegree: HashMap<&str, usize> = ids.iter().map(|id| (*id, 0)).collect();
let mut adj: HashMap<&str, Vec<&str>> = ids.iter().map(|id| (*id, Vec::new())).collect();
for edge in &branch.edges {
if id_set.contains(edge.source.as_str()) && id_set.contains(edge.target.as_str()) {
adj.get_mut(edge.source.as_str())
.unwrap()
.push(&edge.target);
*indegree.get_mut(edge.target.as_str()).unwrap() += 1;
}
}
let mut queue: VecDeque<&str> = ids.iter().copied().filter(|id| indegree[id] == 0).collect();
let mut order = Vec::with_capacity(ids.len());
while let Some(id) = queue.pop_front() {
order.push(id.to_string());
for &next in &adj[id] {
let d = indegree.get_mut(next).unwrap();
*d -= 1;
if *d == 0 {
queue.push_back(next);
}
}
}
if order.len() != ids.len() {
return Err(CompileError::Cycle {
branch_id: branch.branch_id.clone(),
});
}
Ok(order)
}