use crate::model::{
ComponentNode, CompositionGraph, FuncSignature, InstanceInterface, InterfaceConnection,
InterfaceType, TypeArena, ValueType, ValueTypeId, SYNTHETIC_COMPONENT,
};
use anyhow::Result;
use std::collections::HashMap;
use wirm::ir::component::concrete::{ConcreteFuncType, ConcreteType, ConcreteValType};
use wirm::ir::component::refs::{GetCompRefs, GetItemRef};
use wirm::ir::component::visitor::{
walk_structural, ComponentVisitor, ItemKind, ResolvedItem, VisitCtx,
};
use wirm::wasmparser::{
ComponentAlias, ComponentExport, ComponentExternalKind, ComponentInstance, ComponentTypeRef,
PrimitiveValType,
};
use wirm::Component;
pub fn parse_component_imports(buff: &[u8]) -> Result<Vec<(String, Option<String>)>> {
use wirm::wasmparser::ComponentTypeRef;
let component = Component::parse(buff, false, false).expect("Unable to parse");
let mut arena = crate::model::TypeArena::default();
let mut imports = Vec::new();
for import in component.imports.iter() {
if let ComponentTypeRef::Instance(_) = import.ty {
let name = import.name.0.to_string();
let fingerprint = component
.concretize_import(&name)
.and_then(|ct| concrete_to_interface_type(ct, &mut arena))
.map(|it| it.fingerprint(&arena));
imports.push((name, fingerprint));
}
}
Ok(imports)
}
pub fn parse_component(buff: &[u8]) -> Result<CompositionGraph> {
let component = Component::parse(buff, false, false).expect("Unable to parse");
let mut visitor = Visitor::new();
walk_structural(&component, &mut visitor);
visitor.postprocess();
for export in component.exports.iter() {
if export.kind != ComponentExternalKind::Instance {
continue;
}
let name = export.name.0;
{
if let Some(ct) = component.concretize_export(name) {
if let Some(it) = concrete_to_interface_type(ct, &mut visitor.graph.arena) {
let source = visitor
.graph
.component_exports
.get(name)
.map_or(SYNTHETIC_COMPONENT, |e| e.source_instance);
visitor.graph.add_export(name.to_string(), source, Some(it));
}
}
}
}
Ok(visitor.graph)
}
struct Visitor {
curr_comp_num: u32,
comp_id_to_num: Vec<HashMap<u32, u32>>,
graph: CompositionGraph,
next_graph_id: u32,
inst_ptr_to_graph_id: HashMap<usize, u32>,
}
impl Visitor {
pub fn new() -> Self {
Self {
curr_comp_num: 0,
comp_id_to_num: Vec::new(),
graph: CompositionGraph::new(),
next_graph_id: 0,
inst_ptr_to_graph_id: HashMap::new(),
}
}
pub fn postprocess(&mut self) {
let all_node_inst_ids: std::collections::HashSet<u32> =
self.graph.nodes.keys().copied().collect();
for node in self.graph.nodes.values_mut() {
for import in &mut node.imports {
if !import
.source_instance
.is_some_and(|id| all_node_inst_ids.contains(&id))
{
import.is_host_import = true;
import.source_instance = None;
}
}
}
}
}
impl ComponentVisitor<'_> for Visitor {
fn enter_root_component(&mut self, _cx: &VisitCtx<'_>, _component: &Component<'_>) {
self.comp_id_to_num.push(HashMap::new());
}
fn exit_root_component(&mut self, _cx: &VisitCtx<'_>, _component: &Component<'_>) {
self.comp_id_to_num.pop();
}
fn enter_component(&mut self, _cx: &VisitCtx, id: u32, _component: &Component) {
if let Some(outer) = self.comp_id_to_num.last_mut() {
outer.insert(id, self.curr_comp_num);
}
self.curr_comp_num += 1;
self.comp_id_to_num.push(HashMap::new());
}
fn exit_component(&mut self, _: &VisitCtx, _: u32, _component: &Component) {
self.comp_id_to_num.pop();
}
fn visit_comp_instance(&mut self, cx: &VisitCtx, id: u32, instance: &ComponentInstance) {
let name = cx
.lookup_comp_inst_name(id)
.map(|n| n.to_string())
.unwrap_or_else(|| format!("instance_{}", id));
match instance {
ComponentInstance::Instantiate {
component_index,
args,
} => {
let instantiated_comp = if let ResolvedItem::Component(_, comp) =
cx.resolve(&instance.get_comp_refs().first().unwrap().ref_)
{
Some(comp)
} else {
None
};
let comp_num = self.comp_id_to_num.last().unwrap()[component_index];
let mut node = ComponentNode::new(name, *component_index, comp_num);
let graph_id = self.next_graph_id;
self.next_graph_id += 1;
self.inst_ptr_to_graph_id
.insert(instance as *const ComponentInstance as usize, graph_id);
for arg in args.iter() {
let interface_name = arg.name.to_string();
let interface_type =
pull_type_info(&interface_name, &instantiated_comp, &mut self.graph);
let item = cx.resolve(&arg.get_item_ref().ref_);
match item {
ResolvedItem::CompInst(_, inst) => {
let source = self
.inst_ptr_to_graph_id
.get(&(inst as *const ComponentInstance as usize))
.copied();
let connection = InterfaceConnection::from_instance(
interface_name,
source,
interface_type,
&self.graph.arena,
);
node.add_import(connection);
}
ResolvedItem::Import(_id, imp) => {
if let ComponentTypeRef::Instance(_) = imp.ty {
let connection = InterfaceConnection::from_instance(
interface_name,
None,
interface_type,
&self.graph.arena,
);
node.add_import(connection);
}
}
ResolvedItem::Alias(_, alias) => {
resolve_inst_alias(
cx,
alias,
&interface_name,
interface_type,
&mut node,
&self.inst_ptr_to_graph_id,
&self.graph.arena,
);
}
_ => {}
}
}
self.graph.add_node(graph_id, node);
}
ComponentInstance::FromExports(_) => {
}
}
}
fn visit_comp_export(&mut self, cx: &VisitCtx, _: ItemKind, _: u32, export: &ComponentExport) {
if self.comp_id_to_num.len() != 1 {
return;
}
let export_name = export.name.0.to_string();
let item = cx.resolve(&export.get_item_ref().ref_);
match item {
ResolvedItem::CompInst(_, inst) => {
let ptr = inst as *const ComponentInstance as usize;
if let Some(&graph_id) = self.inst_ptr_to_graph_id.get(&ptr) {
let iface_type =
pull_export_type_from_instance(&export_name, inst, &mut self.graph, cx);
self.graph.add_export(export_name, graph_id, iface_type);
}
}
ResolvedItem::Alias(_, alias) => {
let graph = &mut self.graph;
let ptr_map = &self.inst_ptr_to_graph_id;
let outer_comp = cx.curr_component();
resolve_imp_alias(cx, alias, &export_name, graph, ptr_map, outer_comp);
}
_ => {}
}
}
}
fn pull_export_type_from_instance(
export_name: &str,
inst: &ComponentInstance,
graph: &mut CompositionGraph,
cx: &VisitCtx,
) -> Option<InterfaceType> {
let comp_ref = inst.get_comp_refs().into_iter().next()?;
let comp = match cx.resolve(&comp_ref.ref_) {
ResolvedItem::Component(_, c) => c,
_ => return None,
};
let from_export = comp
.concretize_export(export_name)
.and_then(|ct| concrete_to_interface_type(ct, &mut graph.arena));
let has_type_exports = from_export.as_ref().is_some_and(|it| match it {
InterfaceType::Instance(inst) => !inst.type_exports.is_empty(),
_ => true,
});
if has_type_exports {
from_export
} else {
comp.concretize_import(export_name)
.and_then(|ct| concrete_to_interface_type(ct, &mut graph.arena))
.or(from_export)
}
}
fn pull_type_info(
interface_name: &str,
instantiated_comp: &Option<&Component>,
graph: &mut CompositionGraph,
) -> Option<InterfaceType> {
let comp = (*instantiated_comp)?;
let from_import = comp
.concretize_import(interface_name)
.and_then(|ct| concrete_to_interface_type(ct, &mut graph.arena));
let has_type_exports = from_import.as_ref().is_some_and(|it| match it {
InterfaceType::Instance(inst) => !inst.type_exports.is_empty(),
_ => true,
});
if has_type_exports {
from_import
} else {
comp.concretize_export(interface_name)
.and_then(|ct| concrete_to_interface_type(ct, &mut graph.arena))
.or(from_import)
}
}
fn concrete_to_interface_type<'a>(
ty: ConcreteType<'a>,
arena: &mut TypeArena,
) -> Option<InterfaceType> {
match ty {
ConcreteType::Instance {
funcs,
type_exports: te,
} => {
let functions = funcs
.into_iter()
.map(|(name, ft)| (name.to_string(), concrete_to_func_sig(ft, arena)))
.collect();
let type_exports = te
.into_iter()
.map(|(name, cvt)| (name.to_string(), intern(cvt, arena)))
.collect();
Some(InterfaceType::Instance(InstanceInterface {
functions,
type_exports,
}))
}
ConcreteType::Func(ft) => Some(InterfaceType::Func(concrete_to_func_sig(ft, arena))),
ConcreteType::Resource => None,
}
}
fn intern<'a>(ty: ConcreteValType<'a>, arena: &mut TypeArena) -> ValueTypeId {
let vt = concrete_to_val_type(ty, arena);
arena.intern_val(vt)
}
fn concrete_to_func_sig<'a>(ft: ConcreteFuncType<'a>, arena: &mut TypeArena) -> FuncSignature {
let is_async = ft.is_async;
let param_names = ft.params.iter().map(|(name, _)| name.to_string()).collect();
let params = ft
.params
.into_iter()
.map(|(_, ty)| intern(ty, arena))
.collect();
let results = ft.result.map(|ty| intern(ty, arena)).into_iter().collect();
FuncSignature {
is_async,
param_names,
params,
results,
}
}
fn concrete_to_val_type<'a>(ty: ConcreteValType<'a>, arena: &mut TypeArena) -> ValueType {
match ty {
ConcreteValType::Primitive(p) => prim_to_val_type(p),
ConcreteValType::Record(fields) => ValueType::Record(
fields
.into_iter()
.map(|(name, ty)| (name.to_string(), intern(*ty, arena)))
.collect(),
),
ConcreteValType::Variant(cases) => ValueType::Variant(
cases
.into_iter()
.map(|(name, ty)| (name.to_string(), ty.map(|t| intern(*t, arena))))
.collect(),
),
ConcreteValType::List(ty) => ValueType::List(intern(*ty, arena)),
ConcreteValType::FixedLengthList(ty, size) => {
ValueType::FixedSizeList(intern(*ty, arena), size)
}
ConcreteValType::Tuple(types) => {
ValueType::Tuple(types.into_iter().map(|ty| intern(ty, arena)).collect())
}
ConcreteValType::Option(ty) => ValueType::Option(intern(*ty, arena)),
ConcreteValType::Result { ok, err } => ValueType::Result {
ok: ok.map(|t| intern(*t, arena)),
err: err.map(|t| intern(*t, arena)),
},
ConcreteValType::Flags(names) => {
ValueType::Flags(names.iter().map(|s| s.to_string()).collect())
}
ConcreteValType::Enum(names) => {
ValueType::Enum(names.iter().map(|s| s.to_string()).collect())
}
ConcreteValType::Map(key, val) => ValueType::Map(intern(*key, arena), intern(*val, arena)),
ConcreteValType::NamedResource(name) => ValueType::Resource(name.to_string()),
ConcreteValType::AsyncHandle => ValueType::AsyncHandle,
}
}
fn prim_to_val_type(p: PrimitiveValType) -> ValueType {
match p {
PrimitiveValType::Bool => ValueType::Bool,
PrimitiveValType::S8 => ValueType::S8,
PrimitiveValType::U8 => ValueType::U8,
PrimitiveValType::S16 => ValueType::S16,
PrimitiveValType::U16 => ValueType::U16,
PrimitiveValType::S32 => ValueType::S32,
PrimitiveValType::U32 => ValueType::U32,
PrimitiveValType::S64 => ValueType::S64,
PrimitiveValType::U64 => ValueType::U64,
PrimitiveValType::F32 => ValueType::F32,
PrimitiveValType::F64 => ValueType::F64,
PrimitiveValType::Char => ValueType::Char,
PrimitiveValType::String => ValueType::String,
PrimitiveValType::ErrorContext => ValueType::ErrorContext,
}
}
fn resolve_inst_alias(
cx: &VisitCtx,
alias: &ComponentAlias,
interface_name: &str,
interface_type: Option<InterfaceType>,
node: &mut ComponentNode,
inst_ptr_to_graph_id: &HashMap<usize, u32>,
arena: &TypeArena,
) {
let inst_ref = alias.get_item_ref();
match cx.resolve(&inst_ref.ref_) {
ResolvedItem::CompInst(_, inst) => {
let source = inst_ptr_to_graph_id
.get(&(inst as *const ComponentInstance as usize))
.copied();
let connection = InterfaceConnection::from_instance(
interface_name.to_string(),
source,
interface_type,
arena,
);
node.add_import(connection);
}
ResolvedItem::Alias(_, nested_alias) => resolve_inst_alias(
cx,
nested_alias,
interface_name,
interface_type,
node,
inst_ptr_to_graph_id,
arena,
),
_ => {}
}
}
fn resolve_imp_alias(
cx: &VisitCtx,
alias: &ComponentAlias,
export_name: &str,
graph: &mut CompositionGraph,
inst_ptr_to_graph_id: &HashMap<usize, u32>,
outer_comp: &Component,
) {
let inst_ref = alias.get_item_ref();
let resolved = cx.resolve(&inst_ref.ref_);
match resolved {
ResolvedItem::CompInst(_, inst) => {
let ptr = inst as *const ComponentInstance as usize;
if let Some(&graph_id) = inst_ptr_to_graph_id.get(&ptr) {
let mut iface_type = pull_export_type_from_instance(export_name, inst, graph, cx);
let has_type_exports = iface_type.as_ref().is_some_and(|it| match it {
InterfaceType::Instance(inst) => !inst.type_exports.is_empty(),
_ => true,
});
if !has_type_exports {
if let Some(ct) = outer_comp.concretize_export(export_name) {
if let Some(better) = concrete_to_interface_type(ct, &mut graph.arena) {
let better_has_te = match &better {
InterfaceType::Instance(inst) => !inst.type_exports.is_empty(),
_ => false,
};
if better_has_te {
iface_type = Some(better);
}
}
}
}
graph.add_export(export_name.to_string(), graph_id, iface_type);
}
}
ResolvedItem::Alias(_, nested_alias) => resolve_imp_alias(
cx,
nested_alias,
export_name,
graph,
inst_ptr_to_graph_id,
outer_comp,
),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{get_chain_for, is_connection_for};
fn two_middleware_chain_wat() -> &'static str {
r#"(component
(import "wasi:http/handler@0.3.0" (instance $host
(export "handle" (func))
))
(component $middleware-a
(import "wasi:http/handler@0.3.0" (instance $imp
(export "handle" (func))
))
(alias export $imp "handle" (func $f))
(instance $out (export "handle" (func $f)))
(export "wasi:http/handler@0.3.0" (instance $out))
)
(instance $a (instantiate $middleware-a
(with "wasi:http/handler@0.3.0" (instance $host))
))
(alias export $a "wasi:http/handler@0.3.0" (instance $a-out))
(component $middleware-b
(import "wasi:http/handler@0.3.0" (instance $imp
(export "handle" (func))
))
(alias export $imp "handle" (func $f))
(instance $out (export "handle" (func $f)))
(export "wasi:http/handler@0.3.0" (instance $out))
)
(instance $b (instantiate $middleware-b
(with "wasi:http/handler@0.3.0" (instance $a-out))
))
(alias export $b "wasi:http/handler@0.3.0" (instance $b-out))
(export "wasi:http/handler@0.3.0" (instance $b-out))
)"#
}
#[test]
fn test_parse_composed_component() {
let bytes = wat::parse_str(two_middleware_chain_wat()).expect("failed to parse WAT");
let graph = parse_component(&bytes).expect("failed to parse component");
let real_nodes = graph.real_nodes();
assert_eq!(real_nodes.len(), 2, "expected 2 real component nodes");
let http_interface = "wasi:http/handler";
for node in &real_nodes {
assert!(
node.imports
.iter()
.any(|i| is_connection_for(i, http_interface)),
"node '{}' should have a handler import",
node.name
);
}
assert!(
graph
.component_exports
.keys()
.any(|k| k.contains("wasi:http/handler")),
"expected handler export"
);
}
#[test]
fn test_handler_chain_detection() {
let bytes = wat::parse_str(two_middleware_chain_wat()).expect("failed to parse WAT");
let graph = parse_component(&bytes).expect("failed to parse component");
let http_interface = "wasi:http/handler";
let chain = get_chain_for(&graph, http_interface);
assert_eq!(chain.len(), 2, "expected 2 nodes in handler chain");
let first = graph.get_node(chain[0]).expect("first chain node");
assert!(
first
.imports
.iter()
.any(|i| !i.is_host_import && is_connection_for(i, http_interface)),
"first chain node (outermost) should import handler from another component"
);
let last = graph.get_node(chain[1]).expect("last chain node");
assert!(
last.imports
.iter()
.any(|i| i.is_host_import && is_connection_for(i, http_interface)),
"last chain node (innermost) should import handler from host"
);
let first_handler = first
.imports
.iter()
.find(|i| is_connection_for(i, http_interface))
.unwrap();
assert_eq!(
first_handler.source_instance.unwrap(),
chain[1],
"first node's handler source should be the last chain node"
);
}
#[test]
fn test_parse_composed_multiple() {
let bytes = include_bytes!("../../../tests/fixtures/composed-multiple.wasm");
let graph = parse_component(bytes).expect("failed to parse composed-multiple.wasm");
assert!(
!graph.nodes.is_empty(),
"expected at least one component node"
);
}
#[test]
fn test_host_import_detection() {
let bytes = wat::parse_str(two_middleware_chain_wat()).expect("failed to parse WAT");
let graph = parse_component(&bytes).expect("failed to parse component");
let host_interfaces = graph.host_interfaces();
assert!(
host_interfaces
.iter()
.any(|i| i.contains("wasi:http/handler")),
"expected host handler interface, got: {:?}",
host_interfaces
);
}
#[test]
fn fingerprint_from_exports_instance() {
let wat = r#"(component
(import "wasi:http/handler@0.3.0" (instance $host
(export "handle" (func (param "req" u32) (result u32)))
))
(alias export $host "handle" (func $f))
(instance $out (export "handle" (func $f)))
(export "wasi:http/handler@0.3.0" (instance $out))
)"#;
let bytes = wat::parse_str(wat).expect("failed to parse WAT");
let graph = parse_component(&bytes).expect("failed to parse component");
let export = graph
.component_exports
.get("wasi:http/handler@0.3.0")
.expect("expected export for wasi:http/handler@0.3.0");
assert!(
export.fingerprint.is_some(),
"expected non-None fingerprint for FromExports middleware, got None"
);
}
#[test]
fn fingerprint_from_shim_component() {
let wat = r#"(component
(component $shim
(import "handle" (func $h (param "req" u32) (result u32)))
(export "handle" (func $h))
)
(import "handle" (func $h (param "req" u32) (result u32)))
(instance $shim-inst (instantiate $shim
(with "handle" (func $h))
))
(export "wasi:http/handler@0.3.0" (instance $shim-inst))
)"#;
let bytes = wat::parse_str(wat).expect("failed to parse WAT");
let graph = parse_component(&bytes).expect("failed to parse component");
let export = graph
.component_exports
.get("wasi:http/handler@0.3.0")
.expect("expected export for wasi:http/handler@0.3.0");
assert!(
export.fingerprint.is_some(),
"expected non-None fingerprint for shim-component middleware, got None"
);
}
#[test]
fn fingerprint_from_import_reexport() {
let wat = r#"(component
(import "wasi:http/handler@0.3.0" (instance $handler
(export "handle" (func (param "req" u32) (result u32)))
))
(export "wasi:http/handler@0.3.0" (instance $handler))
)"#;
let bytes = wat::parse_str(wat).expect("failed to parse WAT");
let graph = parse_component(&bytes).expect("failed to parse component");
let export = graph
.component_exports
.get("wasi:http/handler@0.3.0")
.expect("expected export for wasi:http/handler@0.3.0");
assert!(
export.fingerprint.is_some(),
"expected non-None fingerprint for import-reexport middleware, got None"
);
}
#[test]
fn fingerprint_matches_between_chain_and_mw() {
let chain_wat = r#"(component
(import "wasi:http/handler@0.3.0" (instance $host
(export "handle" (func (param "req" u32) (result u32)))
))
(alias export $host "handle" (func $f))
(instance $out (export "handle" (func $f)))
(export "wasi:http/handler@0.3.0" (instance $out))
)"#;
let mw_wat = r#"(component
(import "wasi:http/handler@0.3.0" (instance $handler
(export "handle" (func (param "req" u32) (result u32)))
))
(export "wasi:http/handler@0.3.0" (instance $handler))
)"#;
let chain_bytes = wat::parse_str(chain_wat).expect("failed to parse chain WAT");
let mw_bytes = wat::parse_str(mw_wat).expect("failed to parse middleware WAT");
let chain_graph = parse_component(&chain_bytes).expect("failed to parse chain");
let mw_graph = parse_component(&mw_bytes).expect("failed to parse middleware");
let chain_fp = chain_graph
.component_exports
.get("wasi:http/handler@0.3.0")
.and_then(|e| e.fingerprint.as_ref())
.expect("chain should have fingerprint");
let mw_fp = mw_graph
.component_exports
.get("wasi:http/handler@0.3.0")
.and_then(|e| e.fingerprint.as_ref())
.expect("middleware should have fingerprint");
assert_eq!(
chain_fp, mw_fp,
"compatible chain/middleware should have equal fingerprints"
);
}
#[test]
fn parallel_inner_shims_get_distinct_graph_ids() {
let wat = r#"(component
;; Two host-provided interfaces
(import "test:iface/a@0.1.0" (instance $host-a
(export "run" (func))
))
(import "test:iface/b@0.1.0" (instance $host-b
(export "run" (func))
))
;; First wrapper — inner shim gets wasm local id 0 inside this scope
(component $comp-a
(import "test:iface/a@0.1.0" (instance $imp
(export "run" (func))
))
(component $shim-a
(import "test:iface/a@0.1.0" (instance $i
(export "run" (func))
))
(alias export $i "run" (func $f))
(instance $out (export "run" (func $f)))
(export "test:iface/a@0.1.0" (instance $out))
)
(instance $shim-a-inst (instantiate $shim-a
(with "test:iface/a@0.1.0" (instance $imp))
))
(alias export $shim-a-inst "test:iface/a@0.1.0" (instance $a-inner))
(export "test:iface/a@0.1.0" (instance $a-inner))
)
;; Second wrapper — inner shim also gets wasm local id 0 in its own scope
(component $comp-b
(import "test:iface/b@0.1.0" (instance $imp
(export "run" (func))
))
(component $shim-b
(import "test:iface/b@0.1.0" (instance $i
(export "run" (func))
))
(alias export $i "run" (func $f))
(instance $out (export "run" (func $f)))
(export "test:iface/b@0.1.0" (instance $out))
)
(instance $shim-b-inst (instantiate $shim-b
(with "test:iface/b@0.1.0" (instance $imp))
))
(alias export $shim-b-inst "test:iface/b@0.1.0" (instance $b-inner))
(export "test:iface/b@0.1.0" (instance $b-inner))
)
(instance $a (instantiate $comp-a
(with "test:iface/a@0.1.0" (instance $host-a))
))
(alias export $a "test:iface/a@0.1.0" (instance $a-out))
(instance $b (instantiate $comp-b
(with "test:iface/b@0.1.0" (instance $host-b))
))
(alias export $b "test:iface/b@0.1.0" (instance $b-out))
(export "test:iface/a@0.1.0" (instance $a-out))
(export "test:iface/b@0.1.0" (instance $b-out))
)"#;
let bytes = wat::parse_str(wat).expect("failed to parse WAT");
let graph = parse_component(&bytes).expect("failed to parse component");
let export_a = graph
.component_exports
.get("test:iface/a@0.1.0")
.expect("export for test:iface/a@0.1.0 missing");
let export_b = graph
.component_exports
.get("test:iface/b@0.1.0")
.expect("export for test:iface/b@0.1.0 missing");
let src_a = export_a.source_instance;
let src_b = export_b.source_instance;
assert_ne!(
src_a, src_b,
"parallel inner shims at the same wasm local id must map to distinct graph nodes"
);
assert!(
graph.get_node(src_a).is_some(),
"source node for test:iface/a@0.1.0 must exist in graph"
);
assert!(
graph.get_node(src_b).is_some(),
"source node for test:iface/b@0.1.0 must exist in graph"
);
}
#[test]
fn fingerprint_differs_between_chain_and_mw() {
let chain_wat = r#"(component
(import "wasi:http/handler@0.3.0" (instance $host
(export "handle" (func (param "req" u32) (result u32)))
))
(alias export $host "handle" (func $f))
(instance $out (export "handle" (func $f)))
(export "wasi:http/handler@0.3.0" (instance $out))
)"#;
let mw_wat = r#"(component
(import "wasi:http/handler@0.3.0" (instance $handler
(export "handle" (func (param "req" string) (result u32)))
))
(export "wasi:http/handler@0.3.0" (instance $handler))
)"#;
let chain_bytes = wat::parse_str(chain_wat).expect("failed to parse chain WAT");
let mw_bytes = wat::parse_str(mw_wat).expect("failed to parse middleware WAT");
let chain_graph = parse_component(&chain_bytes).expect("failed to parse chain");
let mw_graph = parse_component(&mw_bytes).expect("failed to parse middleware");
let chain_fp = chain_graph
.component_exports
.get("wasi:http/handler@0.3.0")
.and_then(|e| e.fingerprint.as_ref())
.expect("chain should have fingerprint");
let mw_fp = mw_graph
.component_exports
.get("wasi:http/handler@0.3.0")
.and_then(|e| e.fingerprint.as_ref())
.expect("middleware should have fingerprint");
assert_ne!(
chain_fp, mw_fp,
"incompatible chain/middleware should have different fingerprints"
);
}
#[test]
fn cross_interface_aliased_types_populate_type_exports() {
let wat = r#"(component
(component $inner
(import "my:core/types" (instance $types
(type (record (field "order-id" string)))
(export "order" (type (eq 0)))
(type (record (field "order-id" string) (field "amount" u32)))
(export "quote" (type (eq 2)))
))
(alias export $types "order" (type $order))
(alias export $types "quote" (type $quote))
(import "my:service/orders" (instance $svc
(alias outer 1 $order (type (;0;)))
(export "order" (type (eq 0)))
(alias outer 1 $quote (type (;2;)))
(export "quote" (type (eq 2)))
(type (;4;) (func (param "o" 0) (result 2)))
(export "create-order" (func (type 4)))
))
(alias export $svc "create-order" (func $f))
(instance $out (export "create-order" (func $f)))
(export "my:service/orders" (instance $out))
)
(import "my:core/types" (instance $host-types
(type (record (field "order-id" string)))
(export "order" (type (eq 0)))
(type (record (field "order-id" string) (field "amount" u32)))
(export "quote" (type (eq 2)))
))
(alias export $host-types "order" (type $outer-order))
(alias export $host-types "quote" (type $outer-quote))
(import "my:service/orders" (instance $host-svc
(alias outer 1 $outer-order (type (;0;)))
(export "order" (type (eq 0)))
(alias outer 1 $outer-quote (type (;2;)))
(export "quote" (type (eq 2)))
(type (;4;) (func (param "o" 0) (result 2)))
(export "create-order" (func (type 4)))
))
(instance $inst (instantiate $inner
(with "my:core/types" (instance $host-types))
(with "my:service/orders" (instance $host-svc))
))
(alias export $inst "my:service/orders" (instance $out))
(export "my:service/orders" (instance $out))
)"#;
let bytes = wat::parse_str(wat).expect("failed to parse WAT");
let graph = parse_component(&bytes).expect("failed to parse component");
let svc_conn = graph
.nodes
.values()
.flat_map(|n| n.imports.iter())
.find(|c| c.interface_name == "my:service/orders")
.expect("expected some node to import my:service/orders");
let inst = match &svc_conn.interface_type {
Some(crate::model::InterfaceType::Instance(i)) => i,
other => panic!("expected Instance interface_type, got {other:?}"),
};
assert!(
inst.type_exports.contains_key("order"),
"type_exports should include `order` (aliased from my:core/types \
via (alias outer) + (export (type (eq …)))), but got: {:?}",
inst.type_exports.keys().collect::<Vec<_>>()
);
assert!(
inst.type_exports.contains_key("quote"),
"type_exports should include `quote`, but got: {:?}",
inst.type_exports.keys().collect::<Vec<_>>()
);
}
}