use super::topo::GraphError;
use super::{RootPick, TopoOrder, identity_key};
use crate::envelope::CardKind;
pub fn pick_root(order: &TopoOrder) -> Result<RootPick, GraphError> {
if order.nodes.is_empty() {
return Err(GraphError::Empty);
}
let candidates = order
.nodes
.iter()
.filter(|node| {
!order
.edges
.iter()
.any(|edge| identity_key(&edge.to) == identity_key(&node.card_ref))
})
.map(|node| node.card_ref.clone())
.collect::<Vec<_>>();
if let [root] = candidates.as_slice() {
Ok(RootPick { root: root.clone() })
} else {
let services = candidates
.iter()
.filter(|candidate| candidate.kind == CardKind::Service)
.collect::<Vec<_>>();
if let [service] = services.as_slice() {
return Ok(RootPick {
root: (*service).clone(),
});
}
let mut candidates = candidates;
candidates.sort_by_key(identity_key);
Err(GraphError::MultipleRoots { candidates })
}
}
pub fn root_last(mut order: TopoOrder) -> Result<TopoOrder, GraphError> {
let root = pick_root(&order)?.root;
let Some(position) = order
.nodes
.iter()
.position(|node| identity_key(&node.card_ref) == identity_key(&root))
else {
return Err(GraphError::Empty);
};
let root = order.nodes.remove(position);
order.nodes.push(root);
Ok(order)
}
#[cfg(test)]
mod tests {
use super::pick_root;
use crate::envelope::CardKind;
use crate::graph::{Edge, Node, RootPick, TopoOrder};
use crate::reference::CardRef;
use wyrd_semver::VersionBlock;
fn card_ref(kind: CardKind, name: &str) -> CardRef {
CardRef {
kind,
name: name.parse().expect("test card name is valid"),
version: VersionBlock::parse("1.0.0").expect("test version is valid"),
space: Some("default".parse().expect("test space is valid")),
uid: None,
}
}
fn order(nodes: Vec<Node>, edges: Vec<Edge>) -> TopoOrder {
TopoOrder { nodes, edges }
}
fn node(card_ref: CardRef) -> Node {
Node { card_ref }
}
#[test]
fn pick_root_single_root() {
let prompt = card_ref(CardKind::Prompt, "prompt");
let agent = card_ref(CardKind::Agent, "agent");
let service = card_ref(CardKind::Service, "service");
let result = pick_root(&order(
vec![
node(prompt.clone()),
node(agent.clone()),
node(service.clone()),
],
vec![
Edge {
from: service.clone(),
to: agent.clone(),
},
Edge {
from: agent,
to: prompt,
},
],
))
.expect("chain has one root");
assert_eq!(result, RootPick { root: service });
}
#[test]
fn pick_root_prefers_the_only_service_root() {
let left = card_ref(CardKind::Agent, "left");
let right = card_ref(CardKind::Service, "right");
let result = pick_root(&order(
vec![node(right.clone()), node(left.clone())],
Vec::new(),
))
.expect("the only Service is the composite root");
assert_eq!(result, RootPick { root: right });
}
#[test]
fn pick_root_rejects_ambiguous_non_service_roots() {
let left = card_ref(CardKind::Agent, "left");
let right = card_ref(CardKind::Agent, "right");
let error = pick_root(&order(
vec![node(right.clone()), node(left.clone())],
Vec::new(),
))
.expect_err("independent agents are ambiguous");
assert!(matches!(
error,
super::GraphError::MultipleRoots { candidates }
if candidates.contains(&left) && candidates.contains(&right)
));
}
#[test]
fn pick_root_empty_returns_empty_error() {
assert_eq!(
pick_root(&order(Vec::new(), Vec::new())),
Err(super::GraphError::Empty)
);
}
}