Add router-aware endpoint topology APIs

This commit is contained in:
Michael Mikovsky
2026-04-26 16:13:28 -06:00
parent 99d1097f2a
commit 371f3ae492
9 changed files with 669 additions and 54 deletions
+192
View File
@@ -105,3 +105,195 @@ fn leaf_runtime_dispatches_generated_call_procedure() {
.expect("typed response should decode");
assert_eq!(response.text, "echo: hello");
}
#[derive(Default)]
struct TopologyLeaf;
#[leaf(
id = "org.example.v1.topology",
endpoint_struct = TopologyLeaf,
procedures = ["add_child", "remove_child", "connections"]
)]
struct Topology;
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct ChildRequest {
child_path: Vec<String>,
}
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct ConnectionsReply {
parent: Option<Vec<String>>,
children: Vec<Vec<String>>,
}
#[procedures(error = Infallible)]
impl TopologyLeaf {
#[call]
fn add_child(
&mut self,
endpoint: &mut ProtocolEndpoint,
request: ChildRequest,
) -> ConnectionsReply {
endpoint
.upsert_child_route(ChildRoute::registered(request.child_path))
.expect("topology mutation should satisfy direct-child invariants");
ConnectionsReply {
parent: endpoint.parent_path().map(<[String]>::to_vec),
children: endpoint
.child_routes()
.iter()
.map(|child| child.path.clone())
.collect(),
}
}
#[call]
fn remove_child(
&mut self,
endpoint: &mut ProtocolEndpoint,
request: ChildRequest,
) -> ConnectionsReply {
endpoint.remove_child_route(&request.child_path);
ConnectionsReply {
parent: endpoint.parent_path().map(<[String]>::to_vec),
children: endpoint
.child_routes()
.iter()
.map(|child| child.path.clone())
.collect(),
}
}
#[call]
fn connections(&mut self, endpoint: &ProtocolEndpoint) -> ConnectionsReply {
ConnectionsReply {
parent: endpoint.parent_path().map(<[String]>::to_vec),
children: endpoint
.child_routes()
.iter()
.map(|child| child.path.clone())
.collect(),
}
}
}
impl CallLeaf for TopologyLeaf {
type Error = Infallible;
}
#[test]
fn generated_call_procedure_can_query_and_mutate_endpoint_topology() {
let endpoint = ProtocolEndpoint::new(
path(&["agent"]),
Some(Vec::new()),
Vec::new(),
vec![TopologyLeaf::protocol_leaf_spec()],
);
let mut runtime = LeafRuntime::new(endpoint, TopologyLeaf);
let mut controller = ProtocolEndpoint::new(
Vec::new(),
None,
vec![ChildRoute::registered(path(&["agent"]))],
Vec::new(),
);
let add_hook = controller.allocate_hook_id();
let add_child = controller
.send_call(
path(&["agent"]),
Some(TopologyLeaf::protocol_leaf_name()),
TopologyLeaf::protocol_procedure_id("add_child").expect("suffix should resolve"),
Some(add_hook),
encode_call_reply(&ChildRequest {
child_path: path(&["agent", "child"]),
})
.expect("request should encode"),
)
.expect("call should encode");
let EndpointOutcome::Forward {
frame: add_child_frame,
..
} = add_child
else {
panic!("controller should forward add-child call");
};
let add_outcome = runtime
.receive(&Ingress::Parent, add_child_frame)
.expect("runtime should mutate topology");
let [response] = add_outcome.frames.as_slice() else {
panic!("expected add-child response frame");
};
let parsed = decode_frame(response).expect("response should decode");
let reply = decode_call_input::<ConnectionsReply>(
parsed
.deserialize_data()
.expect("reply data should decode")
.data
.as_slice(),
)
.expect("typed reply should decode");
assert_eq!(reply.parent, Some(Vec::new()));
assert_eq!(reply.children, vec![path(&["agent", "child"])]);
assert_eq!(runtime.endpoint().child_routes().len(), 1);
let list_hook = controller.allocate_hook_id();
let list = controller
.send_call(
path(&["agent"]),
Some(TopologyLeaf::protocol_leaf_name()),
TopologyLeaf::protocol_procedure_id("connections").expect("suffix should resolve"),
Some(list_hook),
encode_call_reply(&()).expect("unit request should encode"),
)
.expect("list call should encode");
let EndpointOutcome::Forward {
frame: list_frame, ..
} = list
else {
panic!("controller should forward connections call");
};
let list_outcome = runtime
.receive(&Ingress::Parent, list_frame)
.expect("runtime should return topology snapshot");
let [list_response] = list_outcome.frames.as_slice() else {
panic!("expected connections response frame");
};
let list_reply = decode_call_input::<ConnectionsReply>(
decode_frame(list_response)
.expect("response should decode")
.deserialize_data()
.expect("data should deserialize")
.data
.as_slice(),
)
.expect("typed reply should decode");
assert_eq!(list_reply.children, vec![path(&["agent", "child"])]);
let remove_hook = controller.allocate_hook_id();
let remove = controller
.send_call(
path(&["agent"]),
Some(TopologyLeaf::protocol_leaf_name()),
TopologyLeaf::protocol_procedure_id("remove_child")
.expect("suffix should resolve"),
Some(remove_hook),
encode_call_reply(&ChildRequest {
child_path: path(&["agent", "child"]),
})
.expect("request should encode"),
)
.expect("remove call should encode");
let EndpointOutcome::Forward {
frame: remove_frame,
..
} = remove
else {
panic!("controller should forward remove-child call");
};
runtime
.receive(&Ingress::Parent, remove_frame)
.expect("runtime should prune topology");
assert!(runtime.endpoint().child_routes().is_empty());
}