use std::sync::Arc;
use indexmap::IndexMap;
use super::{Edge, EdgeAnalysis, Graph};
use crate::error::{BuildError, BuildErrors};
use crate::node::NodeKind;
use crate::state::workflow_state::{MergeStrategy, WorkflowState};
use crate::state::{State, StateMerge};
pub struct PendingEdge<'a, S: WorkflowState = State, M: MergeStrategy<S> = StateMerge> {
builder: &'a mut GraphBuilder<S, M>,
edge_index: usize,
}
impl<'a, S: WorkflowState, M: MergeStrategy<S>> PendingEdge<'a, S, M> {
pub fn max_visits(self, n: usize) -> &'a mut GraphBuilder<S, M> {
self.builder.edges[self.edge_index].analysis = Some(EdgeAnalysis {
max_visits: Some(n),
});
self.builder
}
}
pub struct GraphBuilder<S: WorkflowState = State, M: MergeStrategy<S> = StateMerge> {
name: String,
nodes: IndexMap<String, NodeKind<S, M>>,
edges: Vec<Edge<S>>,
start: Option<String>,
end: Option<String>,
canonical_hash: Option<u64>,
}
impl<S: WorkflowState, M: MergeStrategy<S>> GraphBuilder<S, M> {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
nodes: IndexMap::new(),
edges: Vec::new(),
start: None,
end: None,
canonical_hash: None,
}
}
pub fn canonical_hash(&mut self, hash: u64) -> &mut Self {
self.canonical_hash = Some(hash);
self
}
pub fn start(&mut self, node: impl Into<String>) -> &mut Self {
self.start = Some(node.into());
self
}
pub fn end(&mut self, node: impl Into<String>) -> &mut Self {
self.end = Some(node.into());
self
}
pub fn node(&mut self, name: impl Into<String>, kind: NodeKind<S, M>) -> &mut Self {
self.nodes.insert(name.into(), kind);
self
}
pub fn subgraph<Inner: WorkflowState, IM: MergeStrategy<Inner>, L: crate::StateLens<S, Inner>>(
&mut self,
name: impl Into<String>,
spec: crate::SubgraphSpec<S, Inner, IM, L>,
) -> &mut Self
where
S: 'static,
Inner: 'static,
IM: 'static,
L: 'static,
{
let compiled = spec.compile();
self.nodes.insert(name.into(), NodeKind::Subgraph(compiled));
self
}
pub fn edge(
&mut self,
from: impl Into<String>,
to: impl Into<String>,
) -> PendingEdge<'_, S, M> {
let edge_index = self.edges.len();
self.edges.push(Edge {
from: from.into(),
to: to.into(),
condition: None,
analysis: None,
fallback: false,
});
PendingEdge {
builder: self,
edge_index,
}
}
pub fn edge_if(
&mut self,
from: impl Into<String>,
to: impl Into<String>,
condition: impl Fn(&S) -> bool + Send + Sync + 'static,
) -> PendingEdge<'_, S, M> {
let edge_index = self.edges.len();
self.edges.push(Edge {
from: from.into(),
to: to.into(),
condition: Some(Arc::new(condition)),
analysis: None,
fallback: false,
});
PendingEdge {
builder: self,
edge_index,
}
}
pub fn edge_fallback(
&mut self,
from: impl Into<String>,
to: impl Into<String>,
) -> PendingEdge<'_, S, M> {
let edge_index = self.edges.len();
self.edges.push(Edge {
from: from.into(),
to: to.into(),
condition: None,
analysis: None,
fallback: true,
});
PendingEdge {
builder: self,
edge_index,
}
}
pub fn build(self) -> Result<Graph<S, M>, BuildErrors> {
let mut errors = BuildErrors::new();
let start = match self.start {
Some(s) => s,
None => {
errors.push(BuildError::MissingEntryPoint);
return Err(errors);
}
};
let end = match self.end {
Some(s) => s,
None => {
errors.push(BuildError::MissingExitPoint);
return Err(errors);
}
};
let mut seen_nodes = std::collections::HashSet::new();
for name in self.nodes.keys() {
if !seen_nodes.insert(name.clone()) {
errors.push(BuildError::DuplicateNode { id: name.clone() });
}
}
for edge in &self.edges {
if !self.nodes.contains_key(&edge.from) {
errors.push(BuildError::MissingNode {
from: edge.from.clone(),
to: edge.to.clone(),
});
}
if !self.nodes.contains_key(&edge.to) {
errors.push(BuildError::MissingNode {
from: edge.from.clone(),
to: edge.to.clone(),
});
}
}
if !errors.is_empty() {
return Err(errors);
}
let structural_hash = compute_structural_hash(&self.nodes, &self.edges);
let graph = Graph {
name: self.name,
nodes: self.nodes,
edges: self.edges,
start,
end,
canonical_hash: self.canonical_hash.unwrap_or(structural_hash),
};
if let Err(e) = graph.validate() {
return Err(BuildErrors(vec![BuildError::InvalidEdgeDefinition {
from: "graph".to_string(),
to: "graph".to_string(),
reason: e.to_string(),
}]));
}
Ok(graph)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn compile(self) -> Result<Graph<S, M>, BuildErrors> {
use crate::compiler::CompilerPass;
let mut graph = self.build()?;
let mut ctx = crate::compiler::CompilerContext::<S>::new();
let pass = crate::compiler::InlinePass::new();
pass.run(&mut graph, &mut ctx);
if ctx.debug {
tracing::debug!(
inlined = ctx.stats.inlined_count,
skipped = ctx.stats.not_inlined_count,
"compile passes complete"
);
}
Ok(graph)
}
}
pub(crate) fn fnv_hash(s: &str) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325;
for &byte in s.as_bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
fn compute_structural_hash<S: WorkflowState, M: MergeStrategy<S>>(
nodes: &IndexMap<String, NodeKind<S, M>>,
edges: &[Edge<S>],
) -> u64 {
let mut s = String::new();
let mut names: Vec<&str> = nodes.keys().map(|k| k.as_str()).collect();
names.sort();
s.push_str(&names.join(","));
s.push('|');
let mut edge_strs: Vec<String> = edges
.iter()
.map(|e| {
format!(
"{}->{}{:?}{}",
e.from,
e.to,
if e.condition.is_some() { "?" } else { "" },
if e.fallback { "!" } else { "" }
)
})
.collect();
edge_strs.sort();
s.push_str(&edge_strs.join(","));
fnv_hash(&s)
}