mod error;
mod order;
use std::collections::{BTreeMap, HashMap, HashSet};
use crate::pipeline::{PipelineNode, is_io_node};
use crate::pipeline_edge::PipelineEdge;
pub use error::GraphError;
const INPUT: &str = "input";
const OUTPUT: &str = "output";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source<'a> {
LevelInput,
Node(&'a str),
}
#[derive(Debug)]
pub enum Flow<'a> {
Sequence(Vec<&'a PipelineNode>),
Graph(LevelGraph<'a>),
}
impl<'a> Flow<'a> {
pub fn order(&self) -> &[&'a PipelineNode] {
match self {
Flow::Sequence(nodes) => nodes,
Flow::Graph(graph) => &graph.order,
}
}
}
#[derive(Debug)]
pub struct LevelGraph<'a> {
pub order: Vec<&'a PipelineNode>,
pub inputs: BTreeMap<&'a str, Vec<Source<'a>>>,
pub outputs: Vec<Source<'a>>,
}
pub fn level_flow<'a>(
nodes: &'a [PipelineNode],
edges: &'a [PipelineEdge],
) -> Result<Flow<'a>, GraphError> {
let level = Level::new(nodes);
if edges.is_empty() {
return Ok(Flow::Sequence(level.processing));
}
let order = checked_order(&level, nodes, edges).map_err(|mut errors| errors.swap_remove(0))?;
Ok(Flow::Graph(LevelGraph {
inputs: inputs(&level, edges),
outputs: outputs(&level, edges),
order,
}))
}
pub fn level_errors(nodes: &[PipelineNode], edges: &[PipelineEdge]) -> Vec<GraphError> {
if edges.is_empty() {
return Vec::new();
}
checked_order(&Level::new(nodes), nodes, edges)
.err()
.unwrap_or_default()
}
fn checked_order<'a>(
level: &Level<'a>,
nodes: &[PipelineNode],
edges: &[PipelineEdge],
) -> Result<Vec<&'a PipelineNode>, Vec<GraphError>> {
let mut errors = edge_errors(level, edges);
errors.extend(disconnected(nodes, edges));
match order::topological(&level.processing, edges) {
Ok(order) if errors.is_empty() => Ok(order),
Ok(_) => Err(errors),
Err(cycle) => {
errors.push(cycle);
Err(errors)
}
}
}
struct Level<'a> {
by_id: HashMap<&'a str, &'a PipelineNode>,
processing: Vec<&'a PipelineNode>,
has_input: bool,
has_output: bool,
}
impl<'a> Level<'a> {
fn new(nodes: &'a [PipelineNode]) -> Self {
let mut by_id = HashMap::new();
for node in nodes {
by_id.entry(node.id.as_str()).or_insert(node);
}
let has_type = |t: &str| nodes.iter().any(|n| n.node_type == t);
Level {
by_id,
processing: nodes.iter().filter(|n| !is_io_node(&n.node_type)).collect(),
has_input: has_type(INPUT),
has_output: has_type(OUTPUT),
}
}
fn is(&self, id: &str, node_type: &str) -> bool {
self.by_id.get(id).is_some_and(|n| n.node_type == node_type)
}
fn source_of(&self, edge: &'a PipelineEdge) -> Source<'a> {
if self.is(&edge.source, INPUT) {
Source::LevelInput
} else {
Source::Node(&edge.source)
}
}
}
fn edge_errors(level: &Level<'_>, edges: &[PipelineEdge]) -> Vec<GraphError> {
let mut seen = HashSet::new();
let mut errors = Vec::new();
for edge in edges {
if let Err(error) = validate_edge(level, edge) {
errors.push(error);
} else if !seen.insert((edge.source.as_str(), edge.target.as_str())) {
errors.push(GraphError::DuplicateEdge {
edge: edge.id.clone(),
});
}
}
errors
}
fn validate_edge(level: &Level<'_>, edge: &PipelineEdge) -> Result<(), GraphError> {
for id in [&edge.source, &edge.target] {
if !level.by_id.contains_key(id.as_str()) {
let (edge, id) = (edge.id.clone(), id.clone());
return Err(GraphError::UnknownNode { edge, id });
}
}
let edge_id = edge.id.clone();
if level.is(&edge.target, INPUT) {
return Err(GraphError::IntoInput { edge: edge_id });
}
if level.is(&edge.source, OUTPUT) {
return Err(GraphError::FromOutput { edge: edge_id });
}
Ok(())
}
fn disconnected(nodes: &[PipelineNode], edges: &[PipelineEdge]) -> Vec<GraphError> {
let touched: HashSet<&str> = edges
.iter()
.flat_map(|e| [e.source.as_str(), e.target.as_str()])
.collect();
nodes
.iter()
.filter(|n| n.node_type != INPUT && !touched.contains(n.id.as_str()))
.map(|n| GraphError::Disconnected { node: n.id.clone() })
.collect()
}
fn inputs<'a>(level: &Level<'a>, edges: &'a [PipelineEdge]) -> BTreeMap<&'a str, Vec<Source<'a>>> {
let mut inputs: BTreeMap<&str, Vec<Source>> = level
.processing
.iter()
.map(|n| (n.id.as_str(), Vec::new()))
.collect();
for edge in edges {
if let Some(sources) = inputs.get_mut(edge.target.as_str()) {
sources.push(level.source_of(edge));
}
}
if !level.has_input {
feed_first_root(level, &mut inputs);
}
inputs
}
fn feed_first_root<'a>(level: &Level<'a>, inputs: &mut BTreeMap<&'a str, Vec<Source<'a>>>) {
let first_root = level
.processing
.iter()
.find(|n| inputs[n.id.as_str()].is_empty());
if let Some(root) = first_root {
inputs.insert(&root.id, vec![Source::LevelInput]);
}
}
fn outputs<'a>(level: &Level<'a>, edges: &'a [PipelineEdge]) -> Vec<Source<'a>> {
if level.has_output {
return edges
.iter()
.filter(|e| level.is(&e.target, OUTPUT))
.map(|e| level.source_of(e))
.collect();
}
let sources: HashSet<&str> = edges.iter().map(|e| e.source.as_str()).collect();
level
.processing
.iter()
.filter(|n| !sources.contains(n.id.as_str()))
.map(|n| Source::Node(&n.id))
.collect()
}
#[cfg(test)]
mod tests;