use crate::highlights::Highlights;
use crate::model::{CompositionGraph, SYNTHETIC_COMPONENT};
use std::collections::{BTreeSet, VecDeque};
#[derive(Debug, Clone)]
pub struct ExportSubgraph {
pub interface_name: String,
pub source_instance: u32,
pub nodes: BTreeSet<u32>,
pub edges: Vec<SubgraphEdge>,
}
#[derive(Debug, Clone)]
pub struct SubgraphEdge {
pub caller: u32,
pub provider: u32,
pub interface: String,
}
pub fn compute_export_subgraphs(graph: &CompositionGraph) -> Vec<ExportSubgraph> {
graph
.component_exports
.iter()
.filter_map(|(name, info)| {
let src_node = graph.nodes.get(&info.source_instance)?;
if src_node.component_index == SYNTHETIC_COMPONENT {
return None;
}
let source_has_chain_import = src_node
.imports
.iter()
.any(|c| c.interface_name == *name && !c.is_host_import);
let (source_instance, nodes) = if source_has_chain_import {
(
info.source_instance,
reachable_from(graph, info.source_instance),
)
} else {
let chain = crate::build_provider_chain(graph, name);
match chain.first() {
Some(&chain_start) => (chain_start, reachable_from(graph, chain_start)),
None => (
info.source_instance,
reachable_from(graph, info.source_instance),
),
}
};
let edges = collect_edges(graph, &nodes);
Some(ExportSubgraph {
interface_name: name.clone(),
source_instance,
nodes,
edges,
})
})
.collect()
}
pub fn shared_instances(subgraphs: &[ExportSubgraph]) -> BTreeSet<u32> {
let mut seen = BTreeSet::new();
let mut shared = BTreeSet::new();
for sg in subgraphs {
for n in &sg.nodes {
if !seen.insert(*n) {
shared.insert(*n);
}
}
}
shared
}
fn reachable_from(graph: &CompositionGraph, start: u32) -> BTreeSet<u32> {
let mut visited: BTreeSet<u32> = BTreeSet::new();
let mut queue: VecDeque<u32> = VecDeque::from([start]);
while let Some(idx) = queue.pop_front() {
if !visited.insert(idx) {
continue;
}
let Some(node) = graph.nodes.get(&idx) else {
continue;
};
if node.component_index == SYNTHETIC_COMPONENT {
continue;
}
for import in &node.imports {
if import.is_host_import {
continue;
}
if let Some(src) = import.source_instance {
if graph.nodes.contains_key(&src) {
queue.push_back(src);
}
}
}
}
visited
.into_iter()
.filter(|idx| {
graph
.nodes
.get(idx)
.is_some_and(|n| n.component_index != SYNTHETIC_COMPONENT)
})
.collect()
}
pub(crate) fn canonical_ids(
graph: &CompositionGraph,
subgraphs: &[ExportSubgraph],
) -> (BTreeSet<String>, BTreeSet<String>) {
use crate::canonical_id::canonical_edge_id;
let nodes: BTreeSet<String> = graph
.nodes
.values()
.map(|n| n.canonical_id().to_string())
.collect();
let mut edges: BTreeSet<String> = BTreeSet::new();
for sg in subgraphs {
for e in &sg.edges {
let caller = graph.nodes.get(&e.caller).map(|n| n.canonical_id());
let provider = graph.nodes.get(&e.provider).map(|n| n.canonical_id());
if let (Some(c), Some(p)) = (caller, provider) {
edges.insert(canonical_edge_id(&e.interface, Some(c), p));
}
}
if !sg.interface_name.is_empty() {
if let Some(src) = graph.nodes.get(&sg.source_instance) {
edges.insert(canonical_edge_id(
&sg.interface_name,
None,
src.canonical_id(),
));
}
}
}
(nodes, edges)
}
pub(crate) fn filtered_tag_lines(
highlights: Option<&Highlights>,
present_nodes: &BTreeSet<String>,
present_edges: &BTreeSet<String>,
) -> Vec<String> {
highlights
.map(|h| {
h.tag_lines_referenced_by(
present_nodes.iter().map(String::as_str),
present_edges.iter().map(String::as_str),
)
})
.unwrap_or_default()
}
pub(crate) fn collect_edges(graph: &CompositionGraph, nodes: &BTreeSet<u32>) -> Vec<SubgraphEdge> {
let mut edges = Vec::new();
for &caller_idx in nodes {
let Some(caller) = graph.nodes.get(&caller_idx) else {
continue;
};
for import in &caller.imports {
if import.is_host_import {
continue;
}
let Some(provider_idx) = import.source_instance else {
continue;
};
if !nodes.contains(&provider_idx) {
continue;
}
edges.push(SubgraphEdge {
caller: caller_idx,
provider: provider_idx,
interface: import.interface_name.clone(),
});
}
}
edges
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
#[test]
fn simple_chain_single_subgraph() {
let g = simple_chain_graph();
let sgs = compute_export_subgraphs(&g);
assert_eq!(sgs.len(), 1, "simple chain has one export");
let sg = &sgs[0];
assert!(sg.interface_name.contains("wasi:http/handler"));
assert_eq!(sg.nodes, BTreeSet::from([1, 2]));
assert_eq!(sg.edges.len(), 1);
let e = &sg.edges[0];
assert_eq!(e.caller, 2);
assert_eq!(e.provider, 1);
assert!(e.interface.contains("wasi:http/handler"));
}
#[test]
fn two_chain_graph_yields_two_subgraphs() {
let g = two_chain_graph();
let sgs = compute_export_subgraphs(&g);
assert_eq!(sgs.len(), 2);
let by_iface: std::collections::HashMap<&str, &ExportSubgraph> = sgs
.iter()
.map(|sg| (sg.interface_name.as_str(), sg))
.collect();
let http = by_iface
.values()
.find(|sg| sg.interface_name.contains("http"))
.unwrap();
let kv = by_iface
.values()
.find(|sg| sg.interface_name.contains("keyvalue"))
.unwrap();
assert_eq!(http.nodes, BTreeSet::from([1, 2]));
assert_eq!(kv.nodes, BTreeSet::from([3, 4]));
}
#[test]
fn shared_instances_detects_overlap() {
use crate::model::{ComponentNode, CompositionGraph, InterfaceConnection};
let mut g = CompositionGraph::new();
g.add_node(1, ComponentNode::new("$logger".into(), 0, 0));
let mut srv_http = ComponentNode::new("$srv-http".into(), 1, 1);
srv_http.add_import(InterfaceConnection {
interface_name: "wasi:logging/log@0.1.0".into(),
source_instance: Some(1),
is_host_import: false,
interface_type: None,
fingerprint: None,
});
g.add_node(2, srv_http);
let mut cache = ComponentNode::new("$cache".into(), 2, 2);
cache.add_import(InterfaceConnection {
interface_name: "wasi:logging/log@0.1.0".into(),
source_instance: Some(1),
is_host_import: false,
interface_type: None,
fingerprint: None,
});
g.add_node(3, cache);
g.add_export("wasi:http/handler@0.3.0".into(), 2, None);
g.add_export("wasi:keyvalue/store@0.1.0".into(), 3, None);
let sgs = compute_export_subgraphs(&g);
let shared = shared_instances(&sgs);
assert_eq!(shared, BTreeSet::from([1]), "logger should be shared");
}
#[test]
fn synthetic_export_source_skipped() {
use crate::model::{ComponentNode, CompositionGraph, SYNTHETIC_COMPONENT};
let mut g = CompositionGraph::new();
g.add_node(
1,
ComponentNode::new("$synth".into(), SYNTHETIC_COMPONENT, SYNTHETIC_COMPONENT),
);
g.add_export("test:x/y@1.0.0".into(), 1, None);
let sgs = compute_export_subgraphs(&g);
assert!(sgs.is_empty(), "synthetic export source should be filtered");
}
#[test]
fn reachability_excludes_host_imports() {
let g = simple_chain_graph(); let sgs = compute_export_subgraphs(&g);
assert_eq!(sgs[0].nodes.len(), 2);
for e in &sgs[0].edges {
assert!(g.nodes.contains_key(&e.caller));
assert!(g.nodes.contains_key(&e.provider));
}
}
#[test]
fn shim_export_direct_uses_provider_chain() {
let g = shim_export_direct_graph();
let sgs = compute_export_subgraphs(&g);
assert_eq!(sgs.len(), 1);
let sg = &sgs[0];
assert_eq!(sg.source_instance, 1);
assert_eq!(sg.nodes, BTreeSet::from([1]));
assert!(sg.edges.is_empty());
}
#[test]
fn shim_export_one_middleware_uses_provider_chain() {
let g = shim_export_one_middleware_graph();
let sgs = compute_export_subgraphs(&g);
assert_eq!(sgs.len(), 1);
let sg = &sgs[0];
assert_eq!(sg.source_instance, 2);
assert_eq!(sg.nodes, BTreeSet::from([1, 2]));
assert_eq!(sg.edges.len(), 1);
let e = &sg.edges[0];
assert_eq!(e.caller, 2);
assert_eq!(e.provider, 1);
}
#[test]
fn shim_export_three_middleware_uses_provider_chain() {
let g = shim_export_three_middleware_graph();
let sgs = compute_export_subgraphs(&g);
assert_eq!(sgs.len(), 1);
let sg = &sgs[0];
assert_eq!(sg.source_instance, 4);
assert_eq!(sg.nodes, BTreeSet::from([1, 2, 3, 4]));
assert_eq!(sg.edges.len(), 3);
}
}