use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Duration;
use anyhow::Result;
use zenkey::schema::decode::{DecodeError, DecodedPayload, DecoderRegistry};
use zenkey::schema::{SchemaSet, TypeSchema, WireEncoding};
use zenoh::Session;
use crate::registry::SliceSet;
pub struct SchemaStore {
base: String,
timeout: Duration,
sets: Mutex<HashMap<String, Option<SchemaSet>>>,
decoders: DecoderRegistry,
}
impl SchemaStore {
pub fn new(base: impl Into<String>, timeout: Duration) -> Self {
SchemaStore {
base: base.into(),
timeout,
sets: Mutex::new(HashMap::new()),
decoders: DecoderRegistry::new(),
}
}
pub fn decoders_mut(&mut self) -> &mut DecoderRegistry {
&mut self.decoders
}
pub async fn schema_for(
&self,
session: &Session,
producer: &str,
type_name: &str,
) -> Option<TypeSchema> {
{
let sets = self.sets.lock().expect("store lock");
if let Some(cached) = sets.get(producer) {
return cached.as_ref().and_then(|s| s.get(type_name).cloned());
}
}
let fetched = self.fetch(session, producer).await;
let mut sets = self.sets.lock().expect("store lock");
let entry = sets.entry(producer.to_string()).or_insert(fetched);
entry.as_ref().and_then(|s| s.get(type_name).cloned())
}
async fn fetch(&self, session: &Session, producer: &str) -> Option<SchemaSet> {
let key = zenkey::grammar::with_base(
&self.base,
zenkey::selector::fleet_rpc(producer, &["describe"]),
);
let answers = crate::query::fleet_get(session, &self.base, &key, None, self.timeout)
.await
.ok()?;
for a in answers {
if let crate::query::Answer::Value(bytes) = a.answer {
let cow = bytes.to_bytes();
if let Ok(text) = std::str::from_utf8(&cow)
&& let Ok(set) = SchemaSet::parse(text)
{
return Some(set);
}
}
}
None
}
pub fn decode(
&self,
schema: &TypeSchema,
encoding: &WireEncoding,
bytes: &[u8],
) -> Result<DecodedPayload, DecodeError> {
self.decoders.decode(schema, encoding, bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Rendering {
Typed(DecodedPayload),
Structural(String),
}
pub fn resolve_encoding(
sample_encoding: Option<&str>,
registry_encoding: Option<&str>,
bytes: &[u8],
) -> WireEncoding {
if let Some(e) = sample_encoding
&& e != "zenoh/bytes"
{
return WireEncoding::from_encoding_str(e);
}
if let Some(e) = registry_encoding {
return WireEncoding::from_encoding_str(e);
}
match bytes.first() {
Some(b'{' | b'[' | b'"') => WireEncoding::Json,
_ => WireEncoding::Cbor,
}
}
pub fn structural(bytes: &[u8]) -> String {
let looks_json = bytes.first().is_some_and(|b| {
matches!(
b,
b'{' | b'[' | b'"' | b'-' | b'0'..=b'9' | b't' | b'f' | b'n'
)
});
if looks_json && let Ok(v) = serde_json::from_slice::<serde_json::Value>(bytes) {
return serde_json::to_string(&v).unwrap_or_default();
}
if let Ok(v) = ciborium::from_reader::<ciborium::Value, _>(bytes)
&& let Ok(text) = serde_json::to_string(&v)
{
return text;
}
match std::str::from_utf8(bytes) {
Ok(text) if !text.is_empty() => text.to_string(),
_ => format!("<{} bytes>", bytes.len()),
}
}
pub async fn decode_sample(
store: &SchemaStore,
session: &Session,
slices: &SliceSet,
base: &str,
wire_key: &str,
sample_encoding: Option<&str>,
bytes: &[u8],
) -> (Option<String>, Rendering) {
use zenkey::grammar::ClassOrPlane;
let refined = zenkey::grammar::parse_full(base, wire_key).and_then(|parsed| {
let producer = match (&parsed.producer, &parsed.origin) {
(Some(p), _) => p.name().to_string(),
(None, zenkey::grammar::Origin::Service(s)) => {
slices.by_service_origin(s)?.name.clone()
}
_ => return None,
};
let ClassOrPlane::Class(class) = parsed.class else {
return None;
};
let (subject, _) = slices.refine(&producer, class.chunk(), &parsed.subject)?;
Some((
producer,
subject.type_name.clone(),
subject.encoding.clone(),
))
});
let Some((producer, type_name, registry_encoding)) = refined else {
return (None, Rendering::Structural(structural(bytes)));
};
let encoding = resolve_encoding(sample_encoding, registry_encoding.as_deref(), bytes);
match store.schema_for(session, &producer, &type_name).await {
Some(schema) => match store.decode(&schema, &encoding, bytes) {
Ok(decoded) => (Some(type_name), Rendering::Typed(decoded)),
Err(_) => (Some(type_name), Rendering::Structural(structural(bytes))),
},
None => (Some(type_name), Rendering::Structural(structural(bytes))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encoding_resolution_order() {
assert_eq!(
resolve_encoding(Some("application/json"), Some("application/cbor"), b"x"),
WireEncoding::Json
);
assert_eq!(
resolve_encoding(Some("zenoh/bytes"), Some("application/cbor"), b"{"),
WireEncoding::Cbor
);
assert_eq!(
resolve_encoding(None, None, b"{\"a\":1}"),
WireEncoding::Json
);
assert_eq!(resolve_encoding(None, None, &[0xa1]), WireEncoding::Cbor);
}
#[test]
fn structural_rendering_is_honest() {
assert_eq!(structural(b"{\"a\":1}"), "{\"a\":1}");
let mut cbor = Vec::new();
ciborium::into_writer(&serde_json::json!({"x": 1}), &mut cbor).unwrap();
assert!(structural(&cbor).contains("\"x\""));
assert_eq!(structural(&[0xff, 0xfe, 0x00]), "<3 bytes>");
}
}