2026-04-26 14:14:49 -06:00
|
|
|
//! Small end-to-end example for the `leaf!` and `Procedure` macros.
|
2026-04-26 11:25:46 -06:00
|
|
|
//!
|
2026-04-26 14:14:49 -06:00
|
|
|
//! This stays entirely local. A controller endpoint opens one hook-backed procedure against a
|
|
|
|
|
//! single in-process leaf runtime, and the example decodes the returned reply payload.
|
2026-04-26 11:25:46 -06:00
|
|
|
|
2026-04-25 14:46:59 -06:00
|
|
|
use std::error::Error;
|
2026-04-26 14:14:49 -06:00
|
|
|
use std::{collections::BTreeMap, convert::Infallible, string::String};
|
2026-04-25 14:46:59 -06:00
|
|
|
|
2026-04-25 15:35:08 -06:00
|
|
|
use rkyv::{Archive, Deserialize, Serialize};
|
2026-04-25 20:47:37 -06:00
|
|
|
use unshell::protocol::tree::{
|
2026-04-26 14:14:49 -06:00
|
|
|
Call, ChildRoute, EndpointOutcome, HookKey, Ingress, OutgoingData, Procedure, ProcedureEffect,
|
|
|
|
|
ProcedureRuntime, ProcedureStore, ProtocolEndpoint,
|
2026-04-25 20:47:37 -06:00
|
|
|
};
|
2026-04-25 15:35:08 -06:00
|
|
|
use unshell::protocol::{PacketType, decode_frame};
|
2026-04-26 14:14:49 -06:00
|
|
|
use unshell::{Procedure, leaf};
|
2026-04-25 14:46:59 -06:00
|
|
|
|
2026-04-26 14:14:49 -06:00
|
|
|
#[derive(Default)]
|
2026-04-25 15:35:08 -06:00
|
|
|
struct EchoLeaf {
|
2026-04-26 14:14:49 -06:00
|
|
|
sessions: BTreeMap<HookKey, EchoOpen>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 15:19:33 -06:00
|
|
|
#[leaf(id = "org.example.v1.echo", procedures = [EchoOpen], endpoint_struct = EchoLeaf)]
|
|
|
|
|
struct Echo;
|
2026-04-25 15:35:08 -06:00
|
|
|
|
|
|
|
|
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
struct EchoRequest {
|
|
|
|
|
text: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
struct EchoResponse {
|
|
|
|
|
text: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 14:14:49 -06:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Procedure)]
|
|
|
|
|
#[procedure(leaf = EchoLeaf, name = "echo")]
|
|
|
|
|
struct EchoOpen {
|
|
|
|
|
prefix: String,
|
|
|
|
|
return_path: Vec<String>,
|
|
|
|
|
hook_id: u64,
|
|
|
|
|
sent_reply: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProcedureStore<EchoOpen> for EchoLeaf {
|
|
|
|
|
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, EchoOpen> {
|
|
|
|
|
&mut self.sessions
|
2026-04-25 15:35:08 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 14:14:49 -06:00
|
|
|
impl Procedure<EchoLeaf> for EchoOpen {
|
2026-04-25 15:35:08 -06:00
|
|
|
type Error = Infallible;
|
2026-04-26 14:14:49 -06:00
|
|
|
type Input = EchoRequest;
|
|
|
|
|
|
|
|
|
|
fn open(_leaf: &mut EchoLeaf, call: Call<Self::Input>) -> Result<Self, Self::Error> {
|
|
|
|
|
let response_hook = call
|
|
|
|
|
.response_hook
|
|
|
|
|
.expect("example call declares a response hook");
|
|
|
|
|
Ok(Self {
|
|
|
|
|
prefix: call.input.text,
|
|
|
|
|
return_path: response_hook.return_path,
|
|
|
|
|
hook_id: response_hook.hook_id,
|
|
|
|
|
sent_reply: false,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn poll(_leaf: &mut EchoLeaf, session: &mut Self) -> Result<ProcedureEffect, Self::Error> {
|
|
|
|
|
if session.sent_reply {
|
|
|
|
|
return Ok(ProcedureEffect::default());
|
|
|
|
|
}
|
|
|
|
|
session.sent_reply = true;
|
|
|
|
|
Ok(ProcedureEffect::close(vec![OutgoingData {
|
|
|
|
|
dst_path: session.return_path.clone(),
|
|
|
|
|
hook_id: session.hook_id,
|
|
|
|
|
procedure_id: EchoOpen::protocol_procedure_id(),
|
|
|
|
|
data: unshell::protocol::tree::encode_call_reply(&EchoResponse {
|
|
|
|
|
text: format!("echo: {}", session.prefix),
|
|
|
|
|
})
|
|
|
|
|
.expect("response should encode"),
|
|
|
|
|
end_hook: true,
|
|
|
|
|
}]))
|
|
|
|
|
}
|
2026-04-25 15:35:08 -06:00
|
|
|
}
|
2026-04-25 14:46:59 -06:00
|
|
|
|
|
|
|
|
fn path(parts: &[&str]) -> Vec<String> {
|
|
|
|
|
parts.iter().map(|part| (*part).to_owned()).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() -> Result<(), Box<dyn Error>> {
|
2026-04-25 15:35:08 -06:00
|
|
|
let endpoint = ProtocolEndpoint::new(
|
2026-04-25 14:46:59 -06:00
|
|
|
path(&["agent"]),
|
|
|
|
|
Some(Vec::new()),
|
|
|
|
|
Vec::new(),
|
|
|
|
|
vec![EchoLeaf::protocol_leaf_spec()],
|
|
|
|
|
);
|
2026-04-26 14:14:49 -06:00
|
|
|
let mut runtime = ProcedureRuntime::<EchoLeaf, EchoOpen>::new(endpoint, EchoLeaf::default());
|
2026-04-25 14:46:59 -06:00
|
|
|
|
2026-04-25 15:35:08 -06:00
|
|
|
let mut controller = ProtocolEndpoint::new(
|
|
|
|
|
Vec::new(),
|
|
|
|
|
None,
|
|
|
|
|
vec![ChildRoute {
|
|
|
|
|
path: path(&["agent"]),
|
2026-04-25 20:47:37 -06:00
|
|
|
registered: true,
|
2026-04-25 15:35:08 -06:00
|
|
|
}],
|
|
|
|
|
Vec::new(),
|
|
|
|
|
);
|
|
|
|
|
let hook_id = controller.allocate_hook_id();
|
|
|
|
|
let controller_outcome = controller.send_call(
|
2026-04-25 14:46:59 -06:00
|
|
|
path(&["agent"]),
|
|
|
|
|
Some(EchoLeaf::protocol_leaf_name()),
|
2026-04-26 14:14:49 -06:00
|
|
|
EchoOpen::protocol_procedure_id(),
|
2026-04-25 14:46:59 -06:00
|
|
|
Some(hook_id),
|
2026-04-25 15:35:08 -06:00
|
|
|
unshell::protocol::tree::encode_call_reply(&EchoRequest {
|
|
|
|
|
text: String::from("hello leaf"),
|
|
|
|
|
})?,
|
2026-04-25 14:46:59 -06:00
|
|
|
)?;
|
2026-04-25 20:47:37 -06:00
|
|
|
let EndpointOutcome::Forward { frame, .. } = controller_outcome else {
|
2026-04-25 15:35:08 -06:00
|
|
|
return Err("expected controller to forward call".into());
|
|
|
|
|
};
|
2026-04-25 14:46:59 -06:00
|
|
|
|
2026-04-26 14:14:49 -06:00
|
|
|
let receive_outcome = runtime.receive(&Ingress::Parent, frame)?;
|
|
|
|
|
assert!(receive_outcome.frames.is_empty());
|
|
|
|
|
let outcome = runtime.poll()?;
|
2026-04-25 15:35:08 -06:00
|
|
|
let [response_frame] = outcome.frames.as_slice() else {
|
|
|
|
|
return Err("expected one response frame".into());
|
2026-04-25 14:46:59 -06:00
|
|
|
};
|
2026-04-25 15:35:08 -06:00
|
|
|
let parsed = decode_frame(response_frame.as_slice())?;
|
|
|
|
|
assert_eq!(parsed.packet_type(), PacketType::Data);
|
|
|
|
|
let response = unshell::protocol::tree::decode_call_input::<EchoResponse>(
|
|
|
|
|
parsed.deserialize_data()?.data.as_slice(),
|
|
|
|
|
)?;
|
2026-04-25 14:46:59 -06:00
|
|
|
|
2026-04-25 15:35:08 -06:00
|
|
|
assert_eq!(EchoLeaf::protocol_leaf_name(), "org.example.v1.echo");
|
|
|
|
|
assert_eq!(response.text, "echo: hello leaf");
|
2026-04-25 14:46:59 -06:00
|
|
|
|
|
|
|
|
println!(
|
2026-04-25 15:35:08 -06:00
|
|
|
"leaf={} procedure={} response={}",
|
2026-04-25 14:46:59 -06:00
|
|
|
EchoLeaf::protocol_leaf_name(),
|
2026-04-26 14:14:49 -06:00
|
|
|
EchoOpen::protocol_procedure_id(),
|
2026-04-25 15:35:08 -06:00
|
|
|
response.text,
|
2026-04-25 14:46:59 -06:00
|
|
|
);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|