use ::zenoh::bytes::{Encoding, ZBytes};
use ::zenoh::query::{Query, QueryTarget};
use ::zenoh::sample::{Sample, SampleKind};
use ::zenoh::Session;
use anyhow::{bail, ensure, Context, Result};
use serde_json::{json, Value as Json};
use std::time::Duration;
pub const MAX_PAYLOAD: usize = 1024 * 1024;
pub const MAX_REPLIES: usize = 256;
pub const MAX_KEYS: usize = 64;
pub const GET_TIMEOUT: Duration = Duration::from_secs(10);
pub fn config(mode: &str, listen: Option<&str>, connect: Option<&str>) -> Result<::zenoh::Config> {
ensure!(
matches!(mode, "router" | "peer" | "client"),
"mode is router, peer or client"
);
let mut c = ::zenoh::Config::default();
let set = |c: &mut ::zenoh::Config, k: &str, v: String| {
c.insert_json5(k, &v)
.map_err(|e| anyhow::anyhow!("zenoh config {k}: {e}"))
};
set(&mut c, "mode", format!("\"{mode}\""))?;
set(&mut c, "scouting/multicast/enabled", "false".into())?;
set(
&mut c,
"listen/endpoints",
match listen {
Some(l) => format!("[\"{l}\"]"),
None => "[]".into(),
},
)?;
set(
&mut c,
"connect/endpoints",
match connect {
Some(e) => format!("[\"{e}\"]"),
None => "[]".into(),
},
)?;
Ok(c)
}
pub fn keys(v: Option<&Vec<Json>>) -> Result<Vec<String>> {
let Some(list) = v else { return Ok(vec![]) };
ensure!(list.len() <= MAX_KEYS, "at most {MAX_KEYS} key expressions");
list.iter()
.map(|k| {
let k = k.as_str().context("key expressions are strings")?;
::zenoh::key_expr::KeyExpr::try_from(k.to_owned())
.map_err(|e| anyhow::anyhow!("{k:?} is not a key expression: {e}"))?;
Ok(k.to_owned())
})
.collect()
}
pub fn payload_json(bytes: &ZBytes) -> (Json, &'static str) {
match bytes.try_to_string() {
Ok(s) => (json!(s), "utf8"),
Err(_) => (json!(hex::encode(bytes.to_bytes())), "hex"),
}
}
pub fn sample_json(s: &Sample) -> Json {
let (payload, enc) = payload_json(s.payload());
json!({
"key": s.key_expr().as_str(),
"kind": if s.kind() == SampleKind::Put { "put" } else { "delete" },
"payload": payload,
"payload_encoding": enc,
"encoding": s.encoding().to_string(),
})
}
pub fn payload_from(v: &Json) -> Result<Vec<u8>> {
let text = v.get("payload").and_then(Json::as_str).unwrap_or_default();
let data = match v.get("payload_encoding").and_then(Json::as_str) {
None | Some("utf8") => text.as_bytes().to_vec(),
Some("hex") => hex::decode(text).context("payload is not hex")?,
Some(e) => bail!("payload_encoding {e:?} is utf8 or hex"),
};
ensure!(data.len() <= MAX_PAYLOAD, "payload is over 1 MiB");
Ok(data)
}
fn media(v: &Json) -> Encoding {
v.get("encoding")
.and_then(Json::as_str)
.map(Encoding::from)
.unwrap_or(Encoding::TEXT_PLAIN)
}
fn key(v: &Json, field: &str) -> Result<String> {
let k = v
.get(field)
.and_then(Json::as_str)
.with_context(|| format!("{field} is a key expression"))?;
::zenoh::key_expr::KeyExpr::try_from(k.to_owned())
.map_err(|e| anyhow::anyhow!("{k:?} is not a key expression: {e}"))?;
Ok(k.to_owned())
}
pub fn validate(v: &Json) -> Result<()> {
match v["type"].as_str() {
Some("zenoh_put") => {
key(v, "key")?;
payload_from(v)?;
}
Some("zenoh_delete") => {
key(v, "key")?;
}
Some("zenoh_get") => {
let s = v
.get("selector")
.and_then(Json::as_str)
.context("selector is a key expression with optional ?parameters")?;
::zenoh::key_expr::KeyExpr::try_from(
s.split('?').next().unwrap_or_default().to_owned(),
)
.map_err(|e| anyhow::anyhow!("{s:?} is not a selector: {e}"))?;
}
Some("zenoh_reply") => {
if v.get("key").is_some_and(|k| !k.is_null()) {
key(v, "key")?;
}
payload_from(v)?;
}
Some("zenoh_reply_error") => {
payload_from(v)?;
}
_ => bail!("not a Zenoh action"),
}
Ok(())
}
pub async fn perform(session: &Session, v: &Json) -> Result<Option<Json>> {
validate(v)?;
match v["type"].as_str().unwrap_or_default() {
"zenoh_put" => {
session
.put(key(v, "key")?, payload_from(v)?)
.encoding(media(v))
.await
.map_err(|e| anyhow::anyhow!("put: {e}"))?;
Ok(None)
}
"zenoh_delete" => {
session
.delete(key(v, "key")?)
.await
.map_err(|e| anyhow::anyhow!("delete: {e}"))?;
Ok(None)
}
"zenoh_get" => {
let selector = v["selector"].as_str().unwrap_or_default().to_owned();
let replies = session
.get(selector.as_str())
.target(QueryTarget::All)
.timeout(GET_TIMEOUT)
.await
.map_err(|e| anyhow::anyhow!("get: {e}"))?;
let mut out = Vec::new();
while let Ok(reply) = replies.recv_async().await {
if out.len() < MAX_REPLIES {
out.push(match reply.result() {
Ok(s) => sample_json(s),
Err(e) => {
let (payload, enc) = payload_json(e.payload());
json!({"error": payload, "payload_encoding": enc})
}
});
}
}
Ok(Some(json!({"selector": selector, "replies": out})))
}
other => bail!("{other} is not run on a session"),
}
}
pub async fn answer(query: &Query, actions: &[Json]) -> Result<usize> {
let mut n = 0;
for a in actions {
match a["type"].as_str() {
Some("zenoh_reply") => {
let k = match a.get("key").and_then(Json::as_str) {
Some(k) => k.to_owned(),
None => query.key_expr().as_str().to_owned(),
};
query
.reply(k, payload_from(a)?)
.encoding(media(a))
.await
.map_err(|e| anyhow::anyhow!("reply: {e}"))?;
n += 1;
}
Some("zenoh_reply_error") => {
query
.reply_err(payload_from(a)?)
.await
.map_err(|e| anyhow::anyhow!("reply: {e}"))?;
n += 1;
}
_ => {}
}
}
Ok(n)
}
pub fn query_json(q: &Query) -> Json {
let mut out = json!({
"selector": q.selector().to_string(),
"key": q.key_expr().as_str(),
"parameters": q.parameters().as_str(),
});
if let Some(p) = q.payload() {
let (payload, enc) = payload_json(p);
out["payload"] = payload;
out["payload_encoding"] = json!(enc);
}
out
}