use crate::model::{CompositionGraph, ExportInfo, InterfaceConnection};
use std::collections::HashSet;
pub mod canonical_id;
pub mod highlights;
pub mod model;
pub mod output;
pub mod parse;
pub mod subgraph;
pub use canonical_id::{canonical_edge_id, node_by_canonical_id};
pub use highlights::{
HighlightColor, Highlights, Selection, SelectionParseError, TagConflict, UnknownColor,
};
pub use subgraph::{compute_export_subgraphs, shared_instances, ExportSubgraph, SubgraphEdge};
#[cfg(test)]
mod snapshot_tests;
#[cfg(test)]
pub(crate) mod test_utils;
pub fn is_connection_for(conn: &InterfaceConnection, interface_name: &str) -> bool {
conn.interface_name.contains(interface_name)
}
pub fn find_chain_interfaces(graph: &CompositionGraph) -> Vec<String> {
let inter_component: HashSet<&str> = graph
.nodes
.values()
.flat_map(|n| n.imports.iter())
.filter(|c| !c.is_host_import)
.map(|c| c.interface_name.as_str())
.collect();
graph
.component_exports
.keys()
.filter(|name| inter_component.contains(name.as_str()))
.cloned()
.collect()
}
pub fn get_chain_for(graph: &CompositionGraph, interface_name: &str) -> Vec<u32> {
let export_instance = graph
.component_exports
.iter()
.find(|(name, _)| name.contains(interface_name))
.map(
|(
_,
ExportInfo {
source_instance: idx,
..
},
)| *idx,
);
let Some(start) = export_instance else {
return vec![];
};
let start_has_relevant_import = graph.get_node(start).is_some_and(|n| {
n.imports
.iter()
.any(|c| is_connection_for(c, interface_name) && !c.is_host_import)
});
if start_has_relevant_import {
let mut chain = Vec::new();
let mut current: Option<(u32, bool)> = Some((start, false));
let mut visited = std::collections::HashSet::new();
while let Some((idx, is_host)) = current {
if is_host || visited.contains(&idx) {
break;
}
visited.insert(idx);
chain.push(idx);
current = graph.nodes.get(&idx).and_then(|node| {
node.imports
.iter()
.find(|conn| is_connection_for(conn, interface_name) && !conn.is_host_import)
.and_then(|conn| conn.source_instance.map(|src| (src, conn.is_host_import)))
});
}
return chain;
}
build_provider_chain(graph, interface_name)
}
pub(crate) fn build_provider_chain(graph: &CompositionGraph, interface_name: &str) -> Vec<u32> {
let connections: Vec<(u32, u32)> = graph
.nodes
.iter()
.flat_map(|(&consumer_id, node)| {
node.imports
.iter()
.filter(|c| is_connection_for(c, interface_name) && !c.is_host_import)
.filter_map(move |c| c.source_instance.map(|src| (consumer_id, src)))
})
.collect();
if connections.is_empty() {
return vec![];
}
let provider_of: std::collections::HashMap<u32, u32> = connections.iter().copied().collect();
let providers: HashSet<u32> = connections.iter().map(|(_, src)| *src).collect();
let terminal_consumer = connections
.iter()
.map(|(consumer, _)| *consumer)
.find(|c| !providers.contains(c));
let Some(terminal) = terminal_consumer else {
return vec![];
};
let mut chain = Vec::new();
let mut visited = HashSet::new();
let mut current = provider_of.get(&terminal).copied();
while let Some(node_id) = current {
if visited.contains(&node_id) {
break;
}
visited.insert(node_id);
chain.push(node_id);
current = provider_of.get(&node_id).copied();
}
chain
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
#[test]
fn test_find_chain_interfaces_two_chains() {
let graph = two_chain_graph();
let mut chains = find_chain_interfaces(&graph);
chains.sort();
assert_eq!(chains.len(), 2, "should find exactly two chain interfaces");
assert!(
chains.iter().any(|c| c.contains("handler")),
"should find http handler chain"
);
assert!(
chains.iter().any(|c| c.contains("store")),
"should find keyvalue store chain"
);
}
#[test]
fn test_find_chain_interfaces_utility_node_excluded() {
let graph = chain_plus_utility_graph();
let chains = find_chain_interfaces(&graph);
assert_eq!(chains.len(), 1, "utility-only node should not form a chain");
assert!(chains[0].contains("handler"));
}
#[test]
fn test_get_chain_for_http_handler() {
let graph = simple_chain_graph();
let chain = get_chain_for(&graph, "wasi:http/handler@0.3.0");
assert_eq!(
chain,
vec![2, 1],
"http handler chain should walk middleware → srv"
);
}
#[test]
fn test_get_chain_for_long_chain() {
let graph = long_chain_graph();
let chain = get_chain_for(&graph, "wasi:messaging/consumer@0.2.0");
assert_eq!(
chain,
vec![3, 2, 1],
"messaging chain should be in request-flow order (outermost first)"
);
}
#[test]
fn test_get_chain_for_keyvalue_interface() {
let graph = two_chain_graph();
let chain = get_chain_for(&graph, "wasi:keyvalue/store@0.1.0");
assert_eq!(chain, vec![4, 3], "keyvalue chain should walk cache → db");
}
#[test]
fn test_find_chain_interfaces_exported_but_not_imported() {
let mut graph = CompositionGraph::new();
use crate::model::{ComponentNode, InterfaceConnection};
let mut srv = ComponentNode::new("$srv".to_string(), 0, 0);
srv.add_import(InterfaceConnection {
interface_name: "wasi:http/handler@0.3.0".to_string(),
source_instance: None,
is_host_import: true,
interface_type: None,
fingerprint: None,
});
graph.add_node(1, srv);
graph.add_export("wasi:http/handler@0.3.0".to_string(), 1, None);
let chains = find_chain_interfaces(&graph);
assert!(
chains.is_empty(),
"export with no inter-component importers should not be a chain"
);
}
#[test]
fn test_get_chain_for_unknown_interface() {
let graph = simple_chain_graph();
let chain = get_chain_for(&graph, "does:not/exist@0.0.0");
assert!(
chain.is_empty(),
"unknown interface should return empty chain"
);
}
#[test]
fn test_get_chain_for_no_cycle() {
let mut graph = CompositionGraph::new();
use crate::model::{ComponentNode, InterfaceConnection};
let mut a = ComponentNode::new("$a".to_string(), 0, 0);
a.add_import(InterfaceConnection {
interface_name: "test:iface/foo@0.1.0".to_string(),
source_instance: Some(2),
is_host_import: false,
interface_type: None,
fingerprint: None,
});
graph.add_node(1, a);
let mut b = ComponentNode::new("$b".to_string(), 1, 1);
b.add_import(InterfaceConnection {
interface_name: "test:iface/foo@0.1.0".to_string(),
source_instance: Some(1),
is_host_import: false,
interface_type: None,
fingerprint: None,
});
graph.add_node(2, b);
graph.add_export("test:iface/foo@0.1.0".to_string(), 1, None);
let chain = get_chain_for(&graph, "test:iface/foo@0.1.0");
assert!(chain.len() <= 2, "cycle detection should bound the chain");
}
#[test]
fn test_get_chain_for_shim_export_direct() {
let graph = shim_export_direct_graph();
let chain = get_chain_for(&graph, "test:svc/api@1.0.0");
assert_eq!(chain, vec![1], "direct shim export: chain should be [base]");
}
#[test]
fn test_get_chain_for_shim_export_one_middleware() {
let graph = shim_export_one_middleware_graph();
let chain = get_chain_for(&graph, "test:svc/api@1.0.0");
assert_eq!(
chain,
vec![2, 1],
"one-middleware shim export: chain should be [middleware, base]"
);
assert_eq!(
graph.get_node(chain[0]).unwrap().display_label(),
"middleware"
);
assert_eq!(graph.get_node(chain[1]).unwrap().display_label(), "base");
}
#[test]
fn test_get_chain_for_shim_export_three_middlewares() {
let graph = shim_export_three_middleware_graph();
let chain = get_chain_for(&graph, "test:svc/api@1.0.0");
assert_eq!(
chain,
vec![4, 3, 2, 1],
"three-middleware shim export: chain should be [mdl-a, mdl-b, mdl-c, base]"
);
assert_eq!(graph.get_node(chain[0]).unwrap().display_label(), "mdl-a");
assert_eq!(graph.get_node(chain[3]).unwrap().display_label(), "base");
}
#[test]
fn test_find_chain_interfaces_shim_export() {
let graph = shim_export_one_middleware_graph();
let chains = find_chain_interfaces(&graph);
assert!(
chains.iter().any(|c| c.contains("test:svc/api")),
"shim-exported interface should be identified as a chain interface"
);
}
}