use std::path::Path;
use crate::error::{BusError, Result};
use crate::session::apply_phoxal_transport_policy;
#[derive(Debug)]
pub struct Router {
session: zenoh::Session,
}
impl Router {
pub async fn open(listen_endpoints: &[String], config_file: Option<&Path>) -> Result<Self> {
if listen_endpoints.is_empty() {
return Err(BusError::Transport(
"a router needs at least one listen endpoint".to_string(),
));
}
let session = zenoh::open(router_config(listen_endpoints, config_file)?)
.await
.map_err(|error| BusError::Transport(error.to_string()))?;
Ok(Router { session })
}
pub async fn close(self) -> Result<()> {
self.session
.close()
.await
.map_err(|error| BusError::Transport(error.to_string()))
}
}
fn router_config(listen_endpoints: &[String], config_file: Option<&Path>) -> Result<zenoh::Config> {
let mut config = match config_file {
Some(path) => zenoh::Config::from_file(path).map_err(|error| {
BusError::Transport(format!(
"failed to read the router config at {}: {error}",
path.display()
))
})?,
None => zenoh::Config::default(),
};
apply_phoxal_transport_policy(&mut config)?;
let endpoints = serde_json::to_string(listen_endpoints)
.map_err(|error| BusError::Transport(error.to_string()))?;
for (key, value) in [
("mode", "\"router\""),
("listen/endpoints", endpoints.as_str()),
("listen/timeout_ms", "0"),
("listen/exit_on_failure", "true"),
("scouting/delay", "0"),
] {
config
.insert_json5(key, value)
.map_err(|error| BusError::Transport(error.to_string()))?;
}
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn router_config_pins_mode_and_listen_endpoints() {
let config =
router_config(&["tcp/127.0.0.1:7447".to_string()], None).expect("router config");
assert_eq!(config.get_json("mode").expect("mode is set"), "\"router\"");
assert_eq!(
config
.get_json("listen/endpoints")
.expect("listen endpoints are set"),
"[\"tcp/127.0.0.1:7447\"]"
);
}
#[test]
fn router_config_carries_the_same_transport_policy_as_a_client() {
let config =
router_config(&["tcp/127.0.0.1:7447".to_string()], None).expect("router config");
assert_eq!(
config
.get_json("transport/link/tx/lease")
.expect("lease is set"),
"3000"
);
assert_eq!(
config
.get_json("transport/link/tx/keep_alive")
.expect("keepalive is set"),
"4"
);
assert_eq!(
config
.get_json("scouting/multicast/enabled")
.expect("multicast is set"),
"false"
);
}
#[test]
fn phoxal_policy_and_mode_override_an_authored_config_file() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("zenoh.json5");
std::fs::write(
&path,
r#"{ mode: "client", transport: { link: { tx: { lease: 9999 } } } }"#,
)
.expect("write authored router config");
let config = router_config(&["tcp/127.0.0.1:7447".to_string()], Some(&path))
.expect("authored router config");
assert_eq!(config.get_json("mode").expect("mode is set"), "\"router\"");
assert_eq!(
config
.get_json("transport/link/tx/lease")
.expect("lease is set"),
"3000",
"Phoxal transport policy must win over an authored default"
);
}
#[test]
fn an_authored_file_cannot_weaken_the_bound_on_open_guarantee() {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("zenoh.json5");
std::fs::write(
&path,
r#"{ listen: { timeout_ms: 60000, exit_on_failure: false } }"#,
)
.expect("write authored router config");
let config = router_config(&["tcp/127.0.0.1:7447".to_string()], Some(&path))
.expect("authored router config");
assert_eq!(
config
.get_json("listen/timeout_ms")
.expect("listen timeout is pinned"),
"0",
"a background-retry bind would make `open` succeed with nothing listening"
);
assert_eq!(
config
.get_json("listen/exit_on_failure")
.expect("listen exit_on_failure is pinned"),
"true",
"a bind failure must fail `open`, not be swallowed"
);
}
#[test]
fn a_missing_config_file_names_the_path_it_could_not_read() {
let error = router_config(
&["tcp/127.0.0.1:7447".to_string()],
Some(Path::new("/nonexistent/zenoh.json5")),
)
.expect_err("a missing config file must fail");
assert!(
error.to_string().contains("/nonexistent/zenoh.json5"),
"the error must name the unreadable path, got: {error}"
);
}
#[tokio::test]
async fn opening_without_a_listen_endpoint_is_rejected() {
let error = Router::open(&[], None)
.await
.expect_err("a router with nowhere to listen must fail");
assert!(error.to_string().contains("listen endpoint"));
}
}