mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-09 06:47:59 -06:00
Add router-aware endpoint topology APIs
This commit is contained in:
@@ -11,7 +11,9 @@ use crate::protocol::{
|
||||
|
||||
use super::{
|
||||
Endpoint, EndpointError, HookKey, Ingress, LocalEvent, ProtocolEndpoint, ProtocolLeaf,
|
||||
RouteDecision, RouterLeaf,
|
||||
};
|
||||
use super::endpoint::ForwardedFrame;
|
||||
|
||||
/// One typed incoming `Call` passed to a leaf procedure.
|
||||
///
|
||||
@@ -366,6 +368,28 @@ pub struct RuntimeOutcome {
|
||||
pub dropped: bool,
|
||||
}
|
||||
|
||||
/// Frames emitted by the runtime together with their chosen next hops.
|
||||
///
|
||||
/// What it is: the router-oriented variant of [`RuntimeOutcome`], preserving the
|
||||
/// `RouteDecision` for every emitted frame.
|
||||
///
|
||||
/// Why it exists: transport-owning leaves need to know whether each frame should
|
||||
/// go to the parent or to a specific child, not just the encoded bytes.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use unshell::protocol::tree::RoutedRuntimeOutcome;
|
||||
/// let outcome = RoutedRuntimeOutcome::default();
|
||||
/// assert!(outcome.forwarded.is_empty());
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RoutedRuntimeOutcome {
|
||||
/// Forwarded frames paired with the route chosen by the endpoint runtime.
|
||||
pub forwarded: Vec<ForwardedFrame>,
|
||||
/// Whether the endpoint dropped the incoming packet.
|
||||
pub dropped: bool,
|
||||
}
|
||||
|
||||
impl<L> LeafRuntime<L> {
|
||||
/// Builds a runtime from one endpoint and one leaf instance.
|
||||
#[must_use]
|
||||
@@ -453,8 +477,32 @@ where
|
||||
ingress: &Ingress,
|
||||
frame: FrameBytes,
|
||||
) -> Result<RuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
let routed = self.receive_routed(ingress, frame)?;
|
||||
Ok(RuntimeOutcome {
|
||||
frames: routed
|
||||
.forwarded
|
||||
.into_iter()
|
||||
.map(|forwarded| forwarded.frame)
|
||||
.collect(),
|
||||
dropped: routed.dropped,
|
||||
})
|
||||
}
|
||||
|
||||
/// Delivers one inbound frame while preserving route decisions for emitted traffic.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use unshell::protocol::tree::{LeafRuntime, ProtocolEndpoint};
|
||||
/// # struct ExampleLeaf;
|
||||
/// # let _ = core::marker::PhantomData::<LeafRuntime<ExampleLeaf>>;
|
||||
/// ```
|
||||
pub fn receive_routed(
|
||||
&mut self,
|
||||
ingress: &Ingress,
|
||||
frame: FrameBytes,
|
||||
) -> Result<RoutedRuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
let outcome = self.endpoint.receive(ingress, frame)?;
|
||||
self.process_endpoint_outcome(outcome)
|
||||
self.process_endpoint_outcome_routed(outcome)
|
||||
}
|
||||
|
||||
/// Polls the leaf for locally-generated hook traffic and routes any emitted frames.
|
||||
@@ -466,21 +514,45 @@ where
|
||||
/// # let _ = core::marker::PhantomData::<LeafRuntime<ExampleLeaf>>;
|
||||
/// ```
|
||||
pub fn poll(&mut self) -> Result<RuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
let outgoing = self.leaf.poll().map_err(LeafRuntimeError::Leaf)?;
|
||||
self.emit_outgoing(outgoing)
|
||||
let routed = self.poll_routed()?;
|
||||
Ok(RuntimeOutcome {
|
||||
frames: routed
|
||||
.forwarded
|
||||
.into_iter()
|
||||
.map(|forwarded| forwarded.frame)
|
||||
.collect(),
|
||||
dropped: routed.dropped,
|
||||
})
|
||||
}
|
||||
|
||||
fn process_endpoint_outcome(
|
||||
/// Polls the leaf while preserving route decisions for emitted traffic.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use unshell::protocol::tree::{LeafRuntime, ProtocolEndpoint};
|
||||
/// # struct ExampleLeaf;
|
||||
/// # let _ = core::marker::PhantomData::<LeafRuntime<ExampleLeaf>>;
|
||||
/// ```
|
||||
pub fn poll_routed(
|
||||
&mut self,
|
||||
) -> Result<RoutedRuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
let outgoing = self.leaf.poll().map_err(LeafRuntimeError::Leaf)?;
|
||||
self.emit_outgoing_routed(outgoing)
|
||||
}
|
||||
|
||||
fn process_endpoint_outcome_routed(
|
||||
&mut self,
|
||||
outcome: crate::protocol::tree::EndpointOutcome,
|
||||
) -> Result<RuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
) -> Result<RoutedRuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
match outcome {
|
||||
crate::protocol::tree::EndpointOutcome::Forward { frame, .. } => Ok(RuntimeOutcome {
|
||||
frames: vec![frame],
|
||||
dropped: false,
|
||||
}),
|
||||
crate::protocol::tree::EndpointOutcome::Dropped => Ok(RuntimeOutcome {
|
||||
frames: Vec::new(),
|
||||
crate::protocol::tree::EndpointOutcome::Forward { route, frame } => {
|
||||
Ok(RoutedRuntimeOutcome {
|
||||
forwarded: vec![ForwardedFrame { route, frame }],
|
||||
dropped: false,
|
||||
})
|
||||
}
|
||||
crate::protocol::tree::EndpointOutcome::Dropped => Ok(RoutedRuntimeOutcome {
|
||||
forwarded: Vec::new(),
|
||||
dropped: true,
|
||||
}),
|
||||
crate::protocol::tree::EndpointOutcome::Local(event) => self.process_local_event(event),
|
||||
@@ -490,7 +562,7 @@ where
|
||||
fn process_local_event(
|
||||
&mut self,
|
||||
event: LocalEvent,
|
||||
) -> Result<RuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
) -> Result<RoutedRuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
match event {
|
||||
LocalEvent::Call { header, message } => self.process_local_call(header, message),
|
||||
LocalEvent::Data {
|
||||
@@ -510,7 +582,7 @@ where
|
||||
&mut self,
|
||||
header: PacketHeader,
|
||||
message: CallMessage,
|
||||
) -> Result<RuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
) -> Result<RoutedRuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
let CallMessage {
|
||||
procedure_id,
|
||||
data,
|
||||
@@ -528,19 +600,16 @@ where
|
||||
},
|
||||
};
|
||||
|
||||
match self.leaf.dispatch_call(incoming) {
|
||||
match self.leaf.dispatch_call(&mut self.endpoint, incoming) {
|
||||
Ok(CallReply::Reply(bytes)) => {
|
||||
let frames = if let Some(hook) = response_hook {
|
||||
self.send_reply_data(hook, procedure_id, bytes, true)?
|
||||
} else {
|
||||
Vec::new()
|
||||
RoutedRuntimeOutcome::default()
|
||||
};
|
||||
Ok(RuntimeOutcome {
|
||||
frames,
|
||||
dropped: false,
|
||||
})
|
||||
Ok(frames)
|
||||
}
|
||||
Ok(CallReply::NoReply) => Ok(RuntimeOutcome::default()),
|
||||
Ok(CallReply::NoReply) => Ok(RoutedRuntimeOutcome::default()),
|
||||
Err(error) => {
|
||||
// Dispatch failures still emit a protocol fault for the remote caller when a
|
||||
// response hook exists, even though the local runtime also surfaces the error.
|
||||
@@ -555,7 +624,7 @@ where
|
||||
header: PacketHeader,
|
||||
message: DataMessage,
|
||||
hook_key: HookKey,
|
||||
) -> Result<RuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
) -> Result<RoutedRuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
let outgoing = self
|
||||
.leaf
|
||||
.on_data(IncomingData {
|
||||
@@ -564,7 +633,7 @@ where
|
||||
hook_key,
|
||||
})
|
||||
.map_err(LeafRuntimeError::Leaf)?;
|
||||
self.emit_outgoing(outgoing)
|
||||
self.emit_outgoing_routed(outgoing)
|
||||
}
|
||||
|
||||
fn process_local_fault(
|
||||
@@ -572,7 +641,7 @@ where
|
||||
header: PacketHeader,
|
||||
message: crate::protocol::FaultMessage,
|
||||
hook_key: HookKey,
|
||||
) -> Result<RuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
) -> Result<RoutedRuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
self.leaf
|
||||
.on_fault(IncomingFault {
|
||||
header,
|
||||
@@ -580,14 +649,14 @@ where
|
||||
hook_key,
|
||||
})
|
||||
.map_err(LeafRuntimeError::Leaf)?;
|
||||
Ok(RuntimeOutcome::default())
|
||||
Ok(RoutedRuntimeOutcome::default())
|
||||
}
|
||||
|
||||
fn emit_outgoing(
|
||||
fn emit_outgoing_routed(
|
||||
&mut self,
|
||||
outgoing: Vec<OutgoingData>,
|
||||
) -> Result<RuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
let mut runtime = RuntimeOutcome::default();
|
||||
) -> Result<RoutedRuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
let mut runtime = RoutedRuntimeOutcome::default();
|
||||
for packet in outgoing {
|
||||
let endpoint_outcome = self.endpoint.send_data(
|
||||
packet.dst_path,
|
||||
@@ -597,8 +666,8 @@ where
|
||||
packet.end_hook,
|
||||
)?;
|
||||
runtime
|
||||
.frames
|
||||
.extend(self.process_endpoint_outcome(endpoint_outcome)?.frames);
|
||||
.forwarded
|
||||
.extend(self.process_endpoint_outcome_routed(endpoint_outcome)?.forwarded);
|
||||
}
|
||||
Ok(runtime)
|
||||
}
|
||||
@@ -609,7 +678,7 @@ where
|
||||
procedure_id: String,
|
||||
bytes: Vec<u8>,
|
||||
end_hook: bool,
|
||||
) -> Result<Vec<FrameBytes>, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
) -> Result<RoutedRuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
let endpoint_outcome = self.endpoint.send_data(
|
||||
hook.return_path,
|
||||
hook.hook_id,
|
||||
@@ -617,21 +686,65 @@ where
|
||||
bytes,
|
||||
end_hook,
|
||||
)?;
|
||||
Ok(self.process_endpoint_outcome(endpoint_outcome)?.frames)
|
||||
self.process_endpoint_outcome_routed(endpoint_outcome)
|
||||
}
|
||||
|
||||
fn emit_internal_fault_if_possible(
|
||||
&mut self,
|
||||
hook: Option<&HookTarget>,
|
||||
) -> Result<Vec<FrameBytes>, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
) -> Result<RoutedRuntimeOutcome, LeafRuntimeError<<L as CallLeaf>::Error>> {
|
||||
let Some(hook) = hook else {
|
||||
return Ok(Vec::new());
|
||||
return Ok(RoutedRuntimeOutcome::default());
|
||||
};
|
||||
let key = HookKey::new(hook.return_path.clone(), hook.hook_id);
|
||||
let outcome = self
|
||||
.endpoint
|
||||
.emit_fault_if_possible(Some(key), ProtocolFault::INTERNAL_ERROR)?;
|
||||
Ok(self.process_endpoint_outcome(outcome)?.frames)
|
||||
self.process_endpoint_outcome_routed(outcome)
|
||||
}
|
||||
}
|
||||
|
||||
impl<L> LeafRuntime<L>
|
||||
where
|
||||
L: CallLeaf + super::CallProcedures<Error = <L as CallLeaf>::Error> + RouterLeaf,
|
||||
{
|
||||
/// Sends previously forwarded frames through the router leaf's parent/child links.
|
||||
///
|
||||
/// What it is: a small transport bridge from endpoint route decisions to the
|
||||
/// leaf-owned connections.
|
||||
///
|
||||
/// Why it exists: router leaves should be able to reuse the normal protocol
|
||||
/// runtime and still own the concrete forwarding mechanism.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use unshell::protocol::tree::{LeafRuntime, ProtocolEndpoint};
|
||||
/// # struct ExampleLeaf;
|
||||
/// # let _ = core::marker::PhantomData::<LeafRuntime<ExampleLeaf>>;
|
||||
/// ```
|
||||
pub fn route_forwarded(
|
||||
&mut self,
|
||||
forwarded: Vec<ForwardedFrame>,
|
||||
) -> Result<(), <L as RouterLeaf>::RouteError> {
|
||||
for forwarded in forwarded {
|
||||
match forwarded.route {
|
||||
RouteDecision::Parent => {
|
||||
self.leaf
|
||||
.route_to_parent(self.endpoint.path(), forwarded.frame)?;
|
||||
}
|
||||
RouteDecision::Child(index) => {
|
||||
let child_path = self
|
||||
.endpoint
|
||||
.child_routes()
|
||||
.get(index)
|
||||
.map(|child| child.path.clone())
|
||||
.unwrap_or_default();
|
||||
self.leaf.route_to_child(&child_path, forwarded.frame)?;
|
||||
}
|
||||
RouteDecision::Local | RouteDecision::Drop => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ impl ProtocolEndpoint {
|
||||
children: Vec<ChildRoute>,
|
||||
leaves: Vec<LeafSpec>,
|
||||
) -> Self {
|
||||
let has_parent = parent_path.is_some();
|
||||
let registered_child_paths = children
|
||||
.iter()
|
||||
.filter(|child| child.registered)
|
||||
@@ -136,7 +137,8 @@ impl ProtocolEndpoint {
|
||||
|
||||
Self {
|
||||
local_id: None,
|
||||
routing: CompiledRoutes::new(&path, ®istered_child_paths, parent_path.is_some()),
|
||||
parent_path,
|
||||
routing: CompiledRoutes::new(&path, ®istered_child_paths, has_parent),
|
||||
path,
|
||||
children,
|
||||
leaves: leaves
|
||||
@@ -194,6 +196,129 @@ impl ProtocolEndpoint {
|
||||
self.local_id.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the absolute path of this endpoint's direct parent, if one exists.
|
||||
///
|
||||
/// What it is: the currently configured one-hop parent boundary for this
|
||||
/// endpoint.
|
||||
///
|
||||
/// Why it exists: router-style leaves need to expose and inspect the tree edge
|
||||
/// they use for upstream traffic.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use unshell::protocol::tree::ProtocolEndpoint;
|
||||
/// let endpoint = ProtocolEndpoint::new(vec!["worker".into()], Some(Vec::new()), Vec::new(), Vec::new());
|
||||
/// assert_eq!(endpoint.parent_path(), Some([].as_slice()));
|
||||
/// ```
|
||||
pub fn parent_path(&self) -> Option<&[String]> {
|
||||
self.parent_path.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the direct child routes currently known to this endpoint.
|
||||
///
|
||||
/// What it is: the local routing-table inputs for direct descendants.
|
||||
///
|
||||
/// Why it exists: management leaves often need to inspect or mirror the child
|
||||
/// topology they are controlling.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use unshell::protocol::tree::{ChildRoute, ProtocolEndpoint};
|
||||
/// let endpoint = ProtocolEndpoint::new(
|
||||
/// vec!["root".into()],
|
||||
/// None,
|
||||
/// vec![ChildRoute::registered(vec!["root".into(), "child".into()])],
|
||||
/// Vec::new(),
|
||||
/// );
|
||||
/// assert_eq!(endpoint.child_routes().len(), 1);
|
||||
/// ```
|
||||
pub fn child_routes(&self) -> &[ChildRoute] {
|
||||
&self.children
|
||||
}
|
||||
|
||||
/// Replaces the configured direct parent path and recompiles local routing.
|
||||
///
|
||||
/// What it is: the supported way to attach or detach this endpoint from its
|
||||
/// upstream boundary.
|
||||
///
|
||||
/// Why it exists: a router leaf should be able to promote or remove its parent
|
||||
/// connection without rebuilding the entire endpoint.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use unshell::protocol::tree::ProtocolEndpoint;
|
||||
/// let mut endpoint = ProtocolEndpoint::new(vec!["root".into(), "worker".into()], Some(vec!["root".into()]), Vec::new(), Vec::new());
|
||||
/// endpoint.set_parent_path(None)?;
|
||||
/// assert!(endpoint.parent_path().is_none());
|
||||
/// # Ok::<(), unshell::protocol::tree::EndpointError>(())
|
||||
/// ```
|
||||
pub fn set_parent_path(
|
||||
&mut self,
|
||||
parent_path: Option<Vec<String>>,
|
||||
) -> Result<(), EndpointError> {
|
||||
if let Some(path) = parent_path.as_deref() {
|
||||
self.validate_direct_parent_path(path)?;
|
||||
}
|
||||
self.parent_path = parent_path;
|
||||
self.rebuild_routing();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts or updates one direct child route and recompiles local routing.
|
||||
///
|
||||
/// What it is: the supported mutation API for the endpoint's child list.
|
||||
///
|
||||
/// Why it exists: router-management leaves need one invariant-preserving way to
|
||||
/// reflect child connection changes into path routing.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use unshell::protocol::tree::{ChildRoute, ProtocolEndpoint};
|
||||
/// let mut endpoint = ProtocolEndpoint::new(vec!["root".into()], None, Vec::new(), Vec::new());
|
||||
/// endpoint.upsert_child_route(ChildRoute::registered(vec!["root".into(), "child".into()]))?;
|
||||
/// assert_eq!(endpoint.child_routes().len(), 1);
|
||||
/// # Ok::<(), unshell::protocol::tree::EndpointError>(())
|
||||
/// ```
|
||||
pub fn upsert_child_route(&mut self, route: ChildRoute) -> Result<(), EndpointError> {
|
||||
self.validate_direct_child_path(&route.path)?;
|
||||
if let Some(existing) = self.children.iter_mut().find(|child| child.path == route.path) {
|
||||
*existing = route;
|
||||
} else {
|
||||
self.children.push(route);
|
||||
}
|
||||
self.rebuild_routing();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes one direct child route by absolute path and recompiles local routing.
|
||||
///
|
||||
/// What it is: the supported mutation API for pruning a direct descendant.
|
||||
///
|
||||
/// Why it exists: connection-management leaves need to tear down routes without
|
||||
/// mutating the endpoint internals directly.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use unshell::protocol::tree::{ChildRoute, ProtocolEndpoint};
|
||||
/// let mut endpoint = ProtocolEndpoint::new(
|
||||
/// vec!["root".into()],
|
||||
/// None,
|
||||
/// vec![ChildRoute::registered(vec!["root".into(), "child".into()])],
|
||||
/// Vec::new(),
|
||||
/// );
|
||||
/// assert!(endpoint.remove_child_route(&[String::from("root"), String::from("child")]));
|
||||
/// assert!(endpoint.child_routes().is_empty());
|
||||
/// ```
|
||||
pub fn remove_child_route(&mut self, path: &[String]) -> bool {
|
||||
let original_len = self.children.len();
|
||||
self.children.retain(|child| child.path != path);
|
||||
let removed = self.children.len() != original_len;
|
||||
if removed {
|
||||
self.rebuild_routing();
|
||||
}
|
||||
removed
|
||||
}
|
||||
|
||||
/// Registers a procedure that is handled directly by the endpoint.
|
||||
///
|
||||
/// Endpoint-level procedures exist for protocol services that are not attached to one leaf,
|
||||
@@ -230,6 +355,43 @@ impl ProtocolEndpoint {
|
||||
self.hooks.allocate_hook_id(&self.path)
|
||||
}
|
||||
|
||||
fn rebuild_routing(&mut self) {
|
||||
let registered_child_paths = self
|
||||
.children
|
||||
.iter()
|
||||
.filter(|child| child.registered)
|
||||
.map(|child| child.path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
self.routing = CompiledRoutes::new(
|
||||
&self.path,
|
||||
®istered_child_paths,
|
||||
self.parent_path.is_some(),
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_direct_parent_path(&self, parent_path: &[String]) -> Result<(), EndpointError> {
|
||||
let Some((_, expected_parent)) = self.path.split_last() else {
|
||||
return Err(EndpointError::Validation(ValidationError::TopologyInvariant(
|
||||
"root endpoints cannot declare a parent path",
|
||||
)));
|
||||
};
|
||||
if parent_path != expected_parent {
|
||||
return Err(EndpointError::Validation(ValidationError::TopologyInvariant(
|
||||
"parent path must equal the direct path prefix of this endpoint",
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_direct_child_path(&self, child_path: &[String]) -> Result<(), EndpointError> {
|
||||
if child_path.len() != self.path.len() + 1 || !child_path.starts_with(&self.path) {
|
||||
return Err(EndpointError::Validation(ValidationError::TopologyInvariant(
|
||||
"child path must be one direct descendant of this endpoint",
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encodes a call frame without routing it through the local endpoint.
|
||||
///
|
||||
/// This exists for callers that want a fully encoded outbound frame while handling transport
|
||||
|
||||
@@ -177,6 +177,33 @@ pub enum EndpointOutcome {
|
||||
Dropped,
|
||||
}
|
||||
|
||||
/// One framed packet together with the next hop selected by endpoint routing.
|
||||
///
|
||||
/// What it is: a transport-ready frame paired with the resolved direction the
|
||||
/// endpoint chose for it.
|
||||
///
|
||||
/// Why it exists: high-level runtimes often flatten forwarded traffic down to raw
|
||||
/// bytes, but router-host leaves need the route decision so they can choose the
|
||||
/// correct parent or child connection.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use unshell::protocol::FrameBytes;
|
||||
/// use unshell::protocol::tree::{ForwardedFrame, RouteDecision};
|
||||
/// let forwarded = ForwardedFrame {
|
||||
/// route: RouteDecision::Parent,
|
||||
/// frame: FrameBytes::new(),
|
||||
/// };
|
||||
/// assert!(matches!(forwarded.route, RouteDecision::Parent));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ForwardedFrame {
|
||||
/// The next hop selected by the endpoint runtime.
|
||||
pub route: RouteDecision,
|
||||
/// The encoded protocol frame to send over that hop.
|
||||
pub frame: FrameBytes,
|
||||
}
|
||||
|
||||
/// Error surfaced while validating or encoding protocol frames.
|
||||
///
|
||||
/// This exists so endpoint callers can preserve the distinction between malformed wire/archive
|
||||
@@ -288,6 +315,7 @@ pub trait Endpoint {
|
||||
pub struct ProtocolEndpoint {
|
||||
pub(crate) local_id: Option<String>,
|
||||
pub(crate) path: Vec<String>,
|
||||
pub(crate) parent_path: Option<Vec<String>>,
|
||||
pub(crate) children: Vec<ChildRoute>,
|
||||
pub(crate) routing: CompiledRoutes,
|
||||
pub(crate) leaves: BTreeMap<String, LeafSpec>,
|
||||
|
||||
@@ -11,6 +11,6 @@ mod introspection;
|
||||
mod receive;
|
||||
|
||||
pub use core::{
|
||||
ChildRoute, Endpoint, EndpointError, EndpointOutcome, Ingress, LeafSpec, LocalEvent,
|
||||
ChildRoute, Endpoint, EndpointError, EndpointOutcome, ForwardedFrame, Ingress, LeafSpec, LocalEvent,
|
||||
ProtocolEndpoint,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
use super::LeafSpec;
|
||||
use crate::protocol::FrameBytes;
|
||||
|
||||
use super::{ChildRoute, LeafSpec, ProtocolEndpoint};
|
||||
|
||||
/// Static metadata for one application-defined protocol leaf.
|
||||
///
|
||||
@@ -167,7 +169,7 @@ pub trait LeafBinding: ProtocolLeaf {
|
||||
/// impl CallProcedures for ExampleLeaf {
|
||||
/// type Error = core::convert::Infallible;
|
||||
/// fn procedure_suffixes() -> &'static [&'static str] { &["invoke"] }
|
||||
/// fn dispatch_call(&mut self, _call: IncomingCall) -> Result<unshell::protocol::tree::CallReply, DispatchError<Self::Error>> {
|
||||
/// fn dispatch_call(&mut self, _endpoint: &mut unshell::protocol::tree::ProtocolEndpoint, _call: IncomingCall) -> Result<unshell::protocol::tree::CallReply, DispatchError<Self::Error>> {
|
||||
/// Ok(unshell::protocol::tree::CallReply::NoReply)
|
||||
/// }
|
||||
/// }
|
||||
@@ -188,21 +190,77 @@ pub trait CallProcedures: LeafDeclaration {
|
||||
/// use unshell::protocol::tree::{CallProcedures, DispatchError, IncomingCall, ProtocolLeaf};
|
||||
/// struct ExampleLeaf;
|
||||
/// impl ProtocolLeaf for ExampleLeaf { fn leaf_name() -> String { "org.example.v1.echo".into() } }
|
||||
/// impl CallProcedures for ExampleLeaf {
|
||||
/// type Error = core::convert::Infallible;
|
||||
/// fn procedure_suffixes() -> &'static [&'static str] { &["invoke"] }
|
||||
/// fn dispatch_call(&mut self, _call: IncomingCall) -> Result<unshell::protocol::tree::CallReply, DispatchError<Self::Error>> {
|
||||
/// Ok(unshell::protocol::tree::CallReply::NoReply)
|
||||
/// }
|
||||
/// }
|
||||
/// impl CallProcedures for ExampleLeaf {
|
||||
/// type Error = core::convert::Infallible;
|
||||
/// fn procedure_suffixes() -> &'static [&'static str] { &["invoke"] }
|
||||
/// fn dispatch_call(&mut self, _endpoint: &mut unshell::protocol::tree::ProtocolEndpoint, _call: IncomingCall) -> Result<unshell::protocol::tree::CallReply, DispatchError<Self::Error>> {
|
||||
/// Ok(unshell::protocol::tree::CallReply::NoReply)
|
||||
/// }
|
||||
/// }
|
||||
/// # let _ = ExampleLeaf;
|
||||
/// ```
|
||||
fn dispatch_call(
|
||||
&mut self,
|
||||
endpoint: &mut ProtocolEndpoint,
|
||||
call: crate::protocol::tree::IncomingCall,
|
||||
) -> Result<crate::protocol::tree::CallReply, crate::protocol::tree::DispatchError<Self::Error>>;
|
||||
}
|
||||
|
||||
/// Router-facing transport hooks for leaves that own parent/child connections.
|
||||
///
|
||||
/// What it is: an opt-in trait for leaves that want to act as the transport layer
|
||||
/// for one endpoint's forwarded traffic.
|
||||
///
|
||||
/// Why it exists: ordinary leaves only need validated local events, but a router
|
||||
/// leaf also needs to know its active parent/children and where to physically send
|
||||
/// frames chosen by the endpoint's routing logic.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use unshell::protocol::FrameBytes;
|
||||
/// use unshell::protocol::tree::{ChildRoute, RouterLeaf};
|
||||
/// #[derive(Default)]
|
||||
/// struct DemoRouter {
|
||||
/// parent: Option<Vec<String>>,
|
||||
/// children: Vec<ChildRoute>,
|
||||
/// }
|
||||
/// impl unshell::protocol::tree::ProtocolLeaf for DemoRouter {
|
||||
/// fn leaf_name() -> String { "org.example.v1.router".into() }
|
||||
/// }
|
||||
/// impl RouterLeaf for DemoRouter {
|
||||
/// type RouteError = core::convert::Infallible;
|
||||
///
|
||||
/// fn parent_path(&self) -> Option<&[String]> { self.parent.as_deref() }
|
||||
/// fn child_routes(&self) -> &[ChildRoute] { &self.children }
|
||||
/// fn route_to_parent(&mut self, _local_path: &[String], _frame: FrameBytes) -> Result<(), Self::RouteError> { Ok(()) }
|
||||
/// fn route_to_child(&mut self, _child_path: &[String], _frame: FrameBytes) -> Result<(), Self::RouteError> { Ok(()) }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait RouterLeaf: ProtocolLeaf {
|
||||
/// Transport-specific error surfaced while handing a frame to the chosen link.
|
||||
type RouteError;
|
||||
|
||||
/// Returns the currently connected direct parent path, if any.
|
||||
fn parent_path(&self) -> Option<&[String]>;
|
||||
|
||||
/// Returns the currently connected direct child routes.
|
||||
fn child_routes(&self) -> &[ChildRoute];
|
||||
|
||||
/// Sends one routed frame toward the direct parent connection.
|
||||
fn route_to_parent(
|
||||
&mut self,
|
||||
local_path: &[String],
|
||||
frame: FrameBytes,
|
||||
) -> Result<(), Self::RouteError>;
|
||||
|
||||
/// Sends one routed frame toward the chosen direct child connection.
|
||||
fn route_to_child(
|
||||
&mut self,
|
||||
child_path: &[String],
|
||||
frame: FrameBytes,
|
||||
) -> Result<(), Self::RouteError>;
|
||||
}
|
||||
|
||||
/// Builds one canonical dotted leaf id from crate-local metadata plus optional
|
||||
/// user overrides.
|
||||
///
|
||||
|
||||
@@ -16,8 +16,8 @@ mod routing;
|
||||
|
||||
pub use call::{
|
||||
Call, CallLeaf, CallReply, CallResult, DispatchError, IncomingCall, IncomingData,
|
||||
IncomingFault, LeafRuntime, LeafRuntimeError, OutgoingData, RuntimeOutcome, decode_call_input,
|
||||
encode_call_reply,
|
||||
IncomingFault, LeafRuntime, LeafRuntimeError, OutgoingData, RoutedRuntimeOutcome,
|
||||
RuntimeOutcome, decode_call_input, encode_call_reply,
|
||||
};
|
||||
pub use endpoint::{
|
||||
ChildRoute, Endpoint, EndpointError, EndpointOutcome, Ingress, LeafSpec, LocalEvent,
|
||||
@@ -25,7 +25,8 @@ pub use endpoint::{
|
||||
};
|
||||
pub use hook::{ActiveHook, HookConflict, HookKey, HookTable, PendingHook};
|
||||
pub use leaf::{
|
||||
CallProcedures, LeafBinding, LeafDeclaration, ProtocolLeaf, derive_leaf_name, leaf_spec_of,
|
||||
CallProcedures, LeafBinding, LeafDeclaration, ProtocolLeaf, RouterLeaf, derive_leaf_name,
|
||||
leaf_spec_of,
|
||||
};
|
||||
pub use procedure::{
|
||||
Procedure, ProcedureEffect, ProcedureMetadata, ProcedureRuntime, ProcedureRuntimeError,
|
||||
|
||||
Reference in New Issue
Block a user