use std::path::Path;
use anyhow::{Context, Result, bail};
use zenoh::Session;
pub async fn open(connect: &[String], listen: &[String], scouting: bool) -> Result<Session> {
open_with_config(None, connect, listen, Some(scouting)).await
}
pub async fn open_with_config(
file: Option<&Path>,
connect: &[String],
listen: &[String],
scouting: Option<bool>,
) -> Result<Session> {
zenoh::open(build_config(file, connect, listen, scouting)?)
.await
.map_err(|e| anyhow::anyhow!("{e}"))
.context("failed to open Zenoh session")
}
pub(crate) fn explorer_config(
connect: &[String],
listen: &[String],
multicast: bool,
) -> zenoh::Config {
build_config(None, connect, listen, Some(multicast)).expect("no file, no failure")
}
fn build_config(
file: Option<&Path>,
connect: &[String],
listen: &[String],
scouting: Option<bool>,
) -> Result<zenoh::Config> {
let mut config = match file {
Some(path) => {
let config = zenoh::Config::from_file(path)
.map_err(|e| anyhow::anyhow!("{e}"))
.with_context(|| format!("zenoh config {}", path.display()))?;
if let Ok(ns) = config.get_json("namespace")
&& ns != "null"
{
bail!(
"{} sets a session namespace ({ns}) — an explorer runs \
un-namespaced so it sees the wire as it really is \
(RFC 09 §5); remove the namespace from the file, or use \
--base to name the deployment",
path.display()
);
}
config
}
None => zenoh::Config::default(),
};
let json_list = |v: &[String]| {
let items: Vec<String> = v.iter().map(|e| format!("{e:?}")).collect();
format!("[{}]", items.join(","))
};
match scouting {
Some(on) => {
config
.insert_json5("scouting/multicast/enabled", &on.to_string())
.ok();
}
None if file.is_none() => {
config
.insert_json5("scouting/multicast/enabled", "false")
.ok();
}
None => {}
}
if !connect.is_empty() {
config
.insert_json5("connect/endpoints", &json_list(connect))
.ok();
}
if !listen.is_empty() {
config
.insert_json5("listen/endpoints", &json_list(listen))
.ok();
}
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_multicast_bit_follows_the_stated_intent() {
for on in [true, false] {
let config = explorer_config(&[], &[], on);
let json = config.get_json("scouting/multicast/enabled").unwrap();
assert_eq!(json, on.to_string());
}
}
#[test]
fn endpoints_ride_into_the_config() {
let config = explorer_config(&["tcp/127.0.0.1:7447".into()], &[], false);
let json = config.get_json("connect/endpoints").unwrap();
assert!(json.contains("tcp/127.0.0.1:7447"), "{json}");
}
fn temp_config(name: &str, body: &str) -> std::path::PathBuf {
let path = std::env::temp_dir().join(format!("zenkey-fleet-session-{name}.json5"));
std::fs::write(&path, body).unwrap();
path
}
#[test]
fn the_file_is_the_base_and_given_knobs_win() {
let path = temp_config(
"layering",
r#"{ connect: { endpoints: ["tcp/10.0.0.9:7447"] },
scouting: { multicast: { enabled: true } } }"#,
);
let config = build_config(Some(&path), &[], &[], None).unwrap();
assert!(
config
.get_json("connect/endpoints")
.unwrap()
.contains("10.0.0.9"),
);
assert_eq!(
config.get_json("scouting/multicast/enabled").unwrap(),
"true"
);
let config = build_config(
Some(&path),
&["tcp/127.0.0.1:7447".to_string()],
&[],
Some(false),
)
.unwrap();
let endpoints = config.get_json("connect/endpoints").unwrap();
assert!(endpoints.contains("127.0.0.1"), "{endpoints}");
assert!(
!endpoints.contains("10.0.0.9"),
"flag replaces the knob it names"
);
assert_eq!(
config.get_json("scouting/multicast/enabled").unwrap(),
"false"
);
std::fs::remove_file(path).ok();
}
#[test]
fn a_namespaced_file_is_refused_loudly() {
let path = temp_config("namespaced", r#"{ namespace: "acme" }"#);
let err = build_config(Some(&path), &[], &[], None)
.unwrap_err()
.to_string();
assert!(err.contains("RFC 09 §5"), "{err}");
assert!(err.contains("--base"), "{err}");
std::fs::remove_file(path).ok();
}
}