use super::{CheckFinding, finding};
use crate::graph::{GraphError, level_errors};
use crate::pipeline::PipelineNode;
use crate::pipeline_edge::PipelineEdge;
const SAME_LEVEL: &str = "edges only connect nodes at the same level";
const DISCONNECTED: &str = "no edge connects it; when a level has edges, every node must be on one";
const INPUT: &str = "input";
const OUTPUT: &str = "output";
pub(super) struct Level<'a> {
pub nodes: &'a [PipelineNode],
pub edges: &'a [PipelineEdge],
pub owner: Option<&'a PipelineNode>,
}
pub(super) fn edge_findings(level: &Level<'_>, root: &[PipelineNode]) -> Vec<CheckFinding> {
level_errors(level.nodes, level.edges)
.iter()
.map(|error| error_finding(level, root, error))
.collect()
}
fn error_finding(level: &Level<'_>, root: &[PipelineNode], error: &GraphError) -> CheckFinding {
let message = error.to_string();
match error {
GraphError::UnknownNode { edge, id } => unknown_finding(level, root, edge, id),
GraphError::DuplicateEdge { edge } => {
let source = find_edge(level, edge, |_| true).map(|e| &e.source);
on_node(level, source, message)
}
GraphError::IntoInput { edge } => {
let source = find_edge(level, edge, |e| is(level, &e.target, INPUT));
on_node(level, source.map(|e| &e.source), message)
}
GraphError::FromOutput { edge } => {
let target = find_edge(level, edge, |e| is(level, &e.source, OUTPUT));
on_node(level, target.map(|e| &e.target), message)
}
GraphError::Cycle { path } => on_node(level, path.first(), message),
GraphError::Disconnected { node } => on_node(level, Some(node), DISCONNECTED.into()),
}
}
fn unknown_finding(level: &Level<'_>, root: &[PipelineNode], edge: &str, id: &str) -> CheckFinding {
let found = find_edge(level, edge, |e| e.source == id || e.target == id);
let other = found.map(|e| if e.source == id { &e.target } else { &e.source });
let message = unknown_message(edge, id, locate(root, None, id));
match other.and_then(|other| node_at(level, other)) {
Some(node) => finding(node, None, message),
None => orphan(level, id, message),
}
}
fn unknown_message(edge: &str, id: &str, place: Option<Option<&str>>) -> String {
let named = format!("edge \"{edge}\" connects to \"{id}\"");
match place {
None => format!("{named}, which is not a node in this recipe"),
Some(None) => format!("{named}, which is a top-level node; {SAME_LEVEL}"),
Some(Some(container)) => {
format!("{named}, which is inside container \"{container}\"; {SAME_LEVEL}")
}
}
}
fn locate<'a>(
nodes: &'a [PipelineNode],
parent: Option<&'a str>,
id: &str,
) -> Option<Option<&'a str>> {
if nodes.iter().any(|n| n.id == id) {
return Some(parent);
}
nodes.iter().find_map(|n| {
let children = n.children.as_deref().unwrap_or_default();
locate(children, Some(n.id.as_str()), id)
})
}
fn find_edge<'a>(
level: &Level<'a>,
edge: &str,
matches: impl Fn(&PipelineEdge) -> bool,
) -> Option<&'a PipelineEdge> {
level.edges.iter().find(|e| e.id == edge && matches(e))
}
fn node_at<'a>(level: &Level<'a>, id: &str) -> Option<&'a PipelineNode> {
level.nodes.iter().find(|n| n.id == id)
}
fn is(level: &Level<'_>, id: &str, node_type: &str) -> bool {
node_at(level, id).is_some_and(|n| n.node_type == node_type)
}
fn on_node(level: &Level<'_>, id: Option<&String>, message: String) -> CheckFinding {
let id = id.map(String::as_str).unwrap_or_default();
match node_at(level, id) {
Some(node) => finding(node, None, message),
None => orphan(level, id, message),
}
}
fn orphan(level: &Level<'_>, id: &str, message: String) -> CheckFinding {
match level.owner {
Some(owner) => finding(owner, None, message),
None => CheckFinding {
node_id: id.to_string(),
node_type: "unknown".to_string(),
param: None,
message,
},
}
}