2026-04-24 12:32:24 -06:00
|
|
|
//! Path routing helpers and explicit enum tree declarations.
|
|
|
|
|
|
2026-04-25 12:15:38 -06:00
|
|
|
use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
|
2026-04-24 12:32:24 -06:00
|
|
|
|
2026-04-24 13:37:30 -06:00
|
|
|
/// Explicit test tree declaration used for configuration.
|
2026-04-24 12:32:24 -06:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub enum TreeNode {
|
2026-04-25 12:41:10 -06:00
|
|
|
Root {
|
|
|
|
|
children: Vec<Self>,
|
|
|
|
|
},
|
2026-04-24 12:32:24 -06:00
|
|
|
Endpoint {
|
|
|
|
|
segment: String,
|
|
|
|
|
leaves: Vec<LeafNode>,
|
|
|
|
|
children: Vec<Self>,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Leaf declaration used inside the explicit tree enum.
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct LeafNode {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub procedures: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TreeNode {
|
|
|
|
|
pub fn paths(&self) -> Vec<Vec<String>> {
|
|
|
|
|
let mut output = Vec::new();
|
|
|
|
|
self.collect_paths(&[], &mut output);
|
|
|
|
|
output
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collect_paths(&self, prefix: &[String], output: &mut Vec<Vec<String>>) {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Root { children } => {
|
|
|
|
|
output.push(Vec::new());
|
|
|
|
|
for child in children {
|
|
|
|
|
child.collect_paths(&[], output);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Self::Endpoint {
|
|
|
|
|
segment, children, ..
|
|
|
|
|
} => {
|
|
|
|
|
let mut next = prefix.to_vec();
|
|
|
|
|
next.push(segment.clone());
|
|
|
|
|
output.push(next.clone());
|
|
|
|
|
for child in children {
|
|
|
|
|
child.collect_paths(&next, output);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Longest-prefix route decision.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub enum RouteDecision {
|
|
|
|
|
Child(usize),
|
|
|
|
|
Local,
|
|
|
|
|
Parent,
|
|
|
|
|
Drop,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 12:15:38 -06:00
|
|
|
/// One compiled routing table for one endpoint boundary.
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
pub struct CompiledRoutes {
|
|
|
|
|
local_path: Vec<String>,
|
|
|
|
|
has_parent: bool,
|
|
|
|
|
nodes: Vec<RouteTrieNode>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
struct RouteTrieNode {
|
|
|
|
|
best_child: Option<usize>,
|
|
|
|
|
edges: BTreeMap<String, usize>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CompiledRoutes {
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn new(local_path: &[String], child_paths: &[Vec<String>], has_parent: bool) -> Self {
|
|
|
|
|
let mut table = Self {
|
|
|
|
|
local_path: local_path.to_vec(),
|
|
|
|
|
has_parent,
|
|
|
|
|
nodes: vec![RouteTrieNode::default()],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (index, child_path) in child_paths.iter().enumerate() {
|
|
|
|
|
table.insert_child(index, child_path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn insert_child(&mut self, index: usize, child_path: &[String]) {
|
|
|
|
|
if !is_prefix(&self.local_path, child_path) || child_path.len() <= self.local_path.len() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut node_index = 0usize;
|
|
|
|
|
for segment in &child_path[self.local_path.len()..] {
|
|
|
|
|
let next_index = if let Some(next_index) = self.nodes[node_index].edges.get(segment) {
|
|
|
|
|
*next_index
|
|
|
|
|
} else {
|
|
|
|
|
let next_index = self.nodes.len();
|
|
|
|
|
self.nodes.push(RouteTrieNode::default());
|
|
|
|
|
self.nodes[node_index]
|
|
|
|
|
.edges
|
|
|
|
|
.insert(segment.clone(), next_index);
|
|
|
|
|
next_index
|
|
|
|
|
};
|
|
|
|
|
node_index = next_index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.nodes[node_index].best_child = Some(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn route(&self, dst_path: &[String]) -> RouteDecision {
|
|
|
|
|
if !is_prefix(&self.local_path, dst_path) {
|
|
|
|
|
return if self.has_parent {
|
|
|
|
|
RouteDecision::Parent
|
|
|
|
|
} else {
|
|
|
|
|
RouteDecision::Drop
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut best_child = None;
|
|
|
|
|
let mut node_index = 0usize;
|
|
|
|
|
for segment in &dst_path[self.local_path.len()..] {
|
|
|
|
|
let Some(next_index) = self.nodes[node_index].edges.get(segment) else {
|
|
|
|
|
break;
|
|
|
|
|
};
|
|
|
|
|
node_index = *next_index;
|
|
|
|
|
if let Some(index) = self.nodes[node_index].best_child {
|
|
|
|
|
best_child = Some(index);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(index) = best_child {
|
|
|
|
|
return RouteDecision::Child(index);
|
|
|
|
|
}
|
|
|
|
|
if self.local_path == dst_path {
|
|
|
|
|
return RouteDecision::Local;
|
|
|
|
|
}
|
|
|
|
|
RouteDecision::Drop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 13:37:30 -06:00
|
|
|
/// Returns `true` if `prefix` is a path prefix of `path`.
|
2026-04-24 12:32:24 -06:00
|
|
|
pub fn is_prefix(prefix: &[String], path: &[String]) -> bool {
|
|
|
|
|
prefix.len() <= path.len()
|
|
|
|
|
&& prefix
|
|
|
|
|
.iter()
|
|
|
|
|
.zip(path.iter())
|
|
|
|
|
.all(|(left, right)| left == right)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 13:37:30 -06:00
|
|
|
/// Trait for resolving a destination path to a routing decision.
|
|
|
|
|
pub trait RouteProvider {
|
2026-04-24 14:10:03 -06:00
|
|
|
fn route_destination<I>(
|
2026-04-24 13:37:30 -06:00
|
|
|
&self,
|
|
|
|
|
local_path: &[String],
|
2026-04-24 14:10:03 -06:00
|
|
|
child_paths: I,
|
2026-04-24 13:37:30 -06:00
|
|
|
has_parent: bool,
|
|
|
|
|
dst_path: &[String],
|
2026-04-24 14:10:03 -06:00
|
|
|
) -> RouteDecision
|
|
|
|
|
where
|
|
|
|
|
I: IntoIterator,
|
2026-04-24 14:27:55 -06:00
|
|
|
I::Item: AsRef<[String]>;
|
2026-04-24 13:37:30 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Default routing implementation using the protocol's longest-prefix rule.
|
|
|
|
|
pub struct DefaultRouteProvider;
|
|
|
|
|
|
|
|
|
|
impl RouteProvider for DefaultRouteProvider {
|
2026-04-24 14:10:03 -06:00
|
|
|
fn route_destination<I>(
|
2026-04-24 13:37:30 -06:00
|
|
|
&self,
|
|
|
|
|
local_path: &[String],
|
2026-04-24 14:10:03 -06:00
|
|
|
child_paths: I,
|
2026-04-24 13:37:30 -06:00
|
|
|
has_parent: bool,
|
|
|
|
|
dst_path: &[String],
|
2026-04-24 14:10:03 -06:00
|
|
|
) -> RouteDecision
|
|
|
|
|
where
|
|
|
|
|
I: IntoIterator,
|
|
|
|
|
I::Item: AsRef<[String]>,
|
|
|
|
|
{
|
2026-04-25 12:37:54 -06:00
|
|
|
let child_paths = child_paths
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|child| child.as_ref().to_vec())
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
CompiledRoutes::new(local_path, &child_paths, has_parent).route(dst_path)
|
2026-04-24 13:37:30 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 14:10:03 -06:00
|
|
|
pub fn route_destination<I>(
|
2026-04-24 12:32:24 -06:00
|
|
|
local_path: &[String],
|
2026-04-24 14:10:03 -06:00
|
|
|
child_paths: I,
|
2026-04-24 12:32:24 -06:00
|
|
|
has_parent: bool,
|
|
|
|
|
dst_path: &[String],
|
2026-04-24 14:10:03 -06:00
|
|
|
) -> RouteDecision
|
|
|
|
|
where
|
|
|
|
|
I: IntoIterator,
|
|
|
|
|
I::Item: AsRef<[String]>,
|
|
|
|
|
{
|
2026-04-24 13:37:30 -06:00
|
|
|
DefaultRouteProvider.route_destination(local_path, child_paths, has_parent, dst_path)
|
2026-04-24 12:32:24 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use alloc::{string::String, vec};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn longest_prefix_wins() {
|
2026-04-24 13:37:30 -06:00
|
|
|
let provider = DefaultRouteProvider;
|
2026-04-24 12:32:24 -06:00
|
|
|
let children = vec![
|
|
|
|
|
vec![String::from("a")],
|
|
|
|
|
vec![String::from("a"), String::from("b")],
|
|
|
|
|
];
|
|
|
|
|
assert_eq!(
|
2026-04-24 13:37:30 -06:00
|
|
|
provider.route_destination(
|
2026-04-24 12:32:24 -06:00
|
|
|
&Vec::<String>::new(),
|
2026-04-24 14:10:03 -06:00
|
|
|
children,
|
2026-04-24 12:32:24 -06:00
|
|
|
false,
|
|
|
|
|
&[String::from("a"), String::from("b"), String::from("c")]
|
|
|
|
|
),
|
|
|
|
|
RouteDecision::Child(1)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 12:15:38 -06:00
|
|
|
#[test]
|
|
|
|
|
fn compiled_routes_choose_longest_prefix_without_child_scan() {
|
|
|
|
|
let table = CompiledRoutes::new(
|
|
|
|
|
&[String::from("a")],
|
|
|
|
|
&[
|
|
|
|
|
vec![String::from("a"), String::from("b")],
|
|
|
|
|
vec![String::from("a"), String::from("x")],
|
|
|
|
|
],
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
table.route(&[String::from("a"), String::from("b"), String::from("c")]),
|
|
|
|
|
RouteDecision::Child(0)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(table.route(&[String::from("z")]), RouteDecision::Parent);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 12:32:24 -06:00
|
|
|
#[test]
|
|
|
|
|
fn tree_enum_flattens_paths() {
|
|
|
|
|
let tree = TreeNode::Root {
|
|
|
|
|
children: vec![TreeNode::Endpoint {
|
|
|
|
|
segment: String::from("a"),
|
|
|
|
|
leaves: Vec::new(),
|
|
|
|
|
children: vec![TreeNode::Endpoint {
|
|
|
|
|
segment: String::from("b"),
|
|
|
|
|
leaves: Vec::new(),
|
|
|
|
|
children: Vec::new(),
|
|
|
|
|
}],
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
tree.paths(),
|
|
|
|
|
vec![
|
|
|
|
|
Vec::<String>::new(),
|
|
|
|
|
vec![String::from("a")],
|
|
|
|
|
vec![String::from("a"), String::from("b")],
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|