2026-04-24 17:21:39 -06:00
|
|
|
//! Application-procedure handling layered over protocol calls.
|
2026-04-24 17:44:13 -06:00
|
|
|
//!
|
|
|
|
|
//! The core `unshell` runtime validates routing, hooks, and introspection, but
|
|
|
|
|
//! it intentionally does not know what a demo procedure should do. This module
|
|
|
|
|
//! is the thin application layer that turns validated local calls into concrete
|
|
|
|
|
//! demo behavior.
|
2026-04-24 17:21:39 -06:00
|
|
|
|
|
|
|
|
use unshell::protocol::{CallMessage, PacketHeader};
|
|
|
|
|
|
2026-04-25 11:27:29 -06:00
|
|
|
use crate::model::{EndpointProcedureKind, EndpointProcedureSpec, LeafKind, NodeId};
|
2026-04-24 17:21:39 -06:00
|
|
|
|
|
|
|
|
use super::super::super::types::{SimError, Simulation};
|
|
|
|
|
|
|
|
|
|
impl Simulation {
|
2026-04-24 17:44:13 -06:00
|
|
|
/// Handles an application-visible `Call` that the protocol runtime already
|
|
|
|
|
/// accepted and delivered locally.
|
2026-04-24 17:21:39 -06:00
|
|
|
pub(super) fn handle_application_call(
|
|
|
|
|
&mut self,
|
|
|
|
|
node_id: NodeId,
|
2026-04-25 11:27:29 -06:00
|
|
|
header: &PacketHeader,
|
2026-04-24 17:21:39 -06:00
|
|
|
message: &CallMessage,
|
|
|
|
|
) -> Result<(), SimError> {
|
|
|
|
|
let Some(hook) = &message.response_hook else {
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-25 11:27:29 -06:00
|
|
|
if let Some(leaf_name) = &header.dst_leaf {
|
|
|
|
|
let leaf = self.require_leaf(node_id, leaf_name)?.clone();
|
|
|
|
|
match leaf.kind {
|
|
|
|
|
LeafKind::Echo => {
|
2026-04-25 11:46:45 -06:00
|
|
|
let outcome = self.send_endpoint_data(
|
2026-04-25 11:27:29 -06:00
|
|
|
node_id,
|
|
|
|
|
hook.return_path.clone(),
|
|
|
|
|
hook.hook_id,
|
|
|
|
|
message.procedure_id.clone(),
|
|
|
|
|
message.data.clone(),
|
|
|
|
|
true,
|
|
|
|
|
)?;
|
|
|
|
|
self.record_trace(node_id, format!("leaf {leaf_name} echoed {} bytes", message.data.len()));
|
2026-04-25 11:46:45 -06:00
|
|
|
self.process_outcome(node_id, outcome)?;
|
2026-04-25 11:27:29 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 17:44:13 -06:00
|
|
|
// Clone the procedure spec once so later reply generation can borrow the
|
|
|
|
|
// rest of the simulator state freely.
|
2026-04-24 17:21:39 -06:00
|
|
|
let procedure = self
|
|
|
|
|
.lookup_endpoint_procedure(node_id, &message.procedure_id)?
|
|
|
|
|
.clone();
|
|
|
|
|
match procedure.kind {
|
|
|
|
|
EndpointProcedureKind::Ping => {
|
|
|
|
|
let reply = format!("pong from {}", self.node(node_id).display_path());
|
2026-04-25 11:46:45 -06:00
|
|
|
let outcome = self.send_endpoint_data(
|
2026-04-25 11:11:19 -06:00
|
|
|
node_id,
|
|
|
|
|
hook.return_path.clone(),
|
|
|
|
|
hook.hook_id,
|
|
|
|
|
procedure.procedure_id.clone(),
|
|
|
|
|
reply.clone().into_bytes(),
|
|
|
|
|
true,
|
|
|
|
|
)?;
|
2026-04-24 17:21:39 -06:00
|
|
|
self.record_trace(node_id, format!("endpoint sent ping reply: {reply}"));
|
2026-04-25 11:46:45 -06:00
|
|
|
self.process_outcome(node_id, outcome)?;
|
2026-04-24 17:21:39 -06:00
|
|
|
}
|
|
|
|
|
EndpointProcedureKind::ChunkedGreeting => {
|
|
|
|
|
for (index, text) in [
|
|
|
|
|
"chunk 1: hello from the endpoint",
|
|
|
|
|
"chunk 2: routing stayed path-based",
|
|
|
|
|
"chunk 3: hook complete",
|
|
|
|
|
]
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
{
|
2026-04-25 11:46:45 -06:00
|
|
|
let outcome = self.send_endpoint_data(
|
2026-04-25 11:11:19 -06:00
|
|
|
node_id,
|
|
|
|
|
hook.return_path.clone(),
|
|
|
|
|
hook.hook_id,
|
|
|
|
|
procedure.procedure_id.clone(),
|
|
|
|
|
text.as_bytes().to_vec(),
|
|
|
|
|
index == 2,
|
|
|
|
|
)?;
|
2026-04-24 17:21:39 -06:00
|
|
|
self.record_trace(node_id, format!("endpoint sent chunk {}", index + 1));
|
2026-04-25 11:46:45 -06:00
|
|
|
self.process_outcome(node_id, outcome)?;
|
2026-04-24 17:21:39 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EndpointProcedureKind::Chat => {
|
2026-04-24 17:44:13 -06:00
|
|
|
// Persist chat state outside the protocol runtime because the
|
|
|
|
|
// protocol itself does not define chat semantics.
|
2026-04-24 17:21:39 -06:00
|
|
|
self.chat_sessions.insert(
|
|
|
|
|
hook.hook_id,
|
|
|
|
|
super::super::super::types::ChatSession {
|
|
|
|
|
node_id,
|
|
|
|
|
hook_id: hook.hook_id,
|
|
|
|
|
host_path: hook.return_path.clone(),
|
|
|
|
|
procedure_id: procedure.procedure_id.clone(),
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-04-25 11:46:45 -06:00
|
|
|
let outcome = self.send_endpoint_data(
|
2026-04-25 11:11:19 -06:00
|
|
|
node_id,
|
|
|
|
|
hook.return_path.clone(),
|
|
|
|
|
hook.hook_id,
|
|
|
|
|
procedure.procedure_id.clone(),
|
|
|
|
|
b"chat ready".to_vec(),
|
|
|
|
|
false,
|
|
|
|
|
)?;
|
2026-04-24 17:21:39 -06:00
|
|
|
self.record_trace(node_id, "chat handler opened session".to_owned());
|
2026-04-25 11:46:45 -06:00
|
|
|
self.process_outcome(node_id, outcome)?;
|
2026-04-24 17:21:39 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 11:46:45 -06:00
|
|
|
/// Routes one endpoint-originated data packet after application logic decides
|
2026-04-25 11:11:19 -06:00
|
|
|
/// what to send back on an already-validated hook.
|
2026-04-25 11:46:45 -06:00
|
|
|
fn send_endpoint_data(
|
2026-04-25 11:11:19 -06:00
|
|
|
&mut self,
|
|
|
|
|
node_id: NodeId,
|
|
|
|
|
return_path: Vec<String>,
|
|
|
|
|
hook_id: u64,
|
|
|
|
|
procedure_id: String,
|
|
|
|
|
data: Vec<u8>,
|
|
|
|
|
end_hook: bool,
|
2026-04-25 11:46:45 -06:00
|
|
|
) -> Result<unshell::protocol::tree::EndpointOutcome, SimError> {
|
2026-04-25 11:11:19 -06:00
|
|
|
self.nodes[node_id.0]
|
|
|
|
|
.endpoint
|
2026-04-25 11:46:45 -06:00
|
|
|
.send_data(return_path, hook_id, procedure_id, data, end_hook)
|
2026-04-25 11:11:19 -06:00
|
|
|
.map_err(|error| SimError::Protocol(error.to_string()))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 17:44:13 -06:00
|
|
|
/// Resolves one endpoint procedure from the ground-truth node metadata.
|
2026-04-24 17:21:39 -06:00
|
|
|
pub(super) fn lookup_endpoint_procedure(
|
|
|
|
|
&self,
|
|
|
|
|
node_id: NodeId,
|
|
|
|
|
procedure_id: &str,
|
|
|
|
|
) -> Result<&EndpointProcedureSpec, SimError> {
|
|
|
|
|
self.node(node_id)
|
|
|
|
|
.endpoint_procedures
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|procedure| procedure.procedure_id == procedure_id)
|
|
|
|
|
.ok_or_else(|| SimError::UnknownProcedure {
|
|
|
|
|
node_path: self.node(node_id).display_path(),
|
|
|
|
|
procedure_id: procedure_id.to_owned(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 17:44:13 -06:00
|
|
|
/// Ensures one named leaf exists on the target node.
|
2026-04-24 17:21:39 -06:00
|
|
|
pub(crate) fn require_leaf(
|
|
|
|
|
&self,
|
|
|
|
|
node_id: NodeId,
|
|
|
|
|
leaf_name: &str,
|
|
|
|
|
) -> Result<&crate::model::LeafSpec, SimError> {
|
|
|
|
|
self.node(node_id)
|
|
|
|
|
.leaves
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|leaf| leaf.name == leaf_name)
|
|
|
|
|
.ok_or_else(|| SimError::UnknownLeaf {
|
|
|
|
|
node_path: self.node(node_id).display_path(),
|
|
|
|
|
leaf_name: leaf_name.to_owned(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 17:44:13 -06:00
|
|
|
/// Ensures one endpoint procedure exists on the target node.
|
2026-04-24 17:21:39 -06:00
|
|
|
pub(crate) fn require_endpoint_procedure(
|
|
|
|
|
&self,
|
|
|
|
|
node_id: NodeId,
|
|
|
|
|
procedure_id: &str,
|
|
|
|
|
) -> Result<(), SimError> {
|
|
|
|
|
self.lookup_endpoint_procedure(node_id, procedure_id)
|
|
|
|
|
.map(|_| ())
|
|
|
|
|
}
|
|
|
|
|
}
|