1use std::collections::HashMap;
12use std::sync::Mutex;
13use std::time::Duration;
14
15use anyhow::Result;
16use zenkey::schema::decode::{DecodeError, DecodedPayload, DecoderRegistry};
17use zenkey::schema::{SchemaSet, TypeSchema, WireEncoding};
18use zenoh::Session;
19
20use crate::registry::SliceSet;
21
22pub struct SchemaStore {
24 base: String,
25 timeout: Duration,
26 sets: Mutex<HashMap<String, Option<SchemaSet>>>,
28 decoders: DecoderRegistry,
29}
30
31impl SchemaStore {
32 pub fn new(base: impl Into<String>, timeout: Duration) -> Self {
33 SchemaStore {
34 base: base.into(),
35 timeout,
36 sets: Mutex::new(HashMap::new()),
37 decoders: DecoderRegistry::new(),
38 }
39 }
40
41 pub fn decoders_mut(&mut self) -> &mut DecoderRegistry {
43 &mut self.decoders
44 }
45
46 pub async fn schema_for(
52 &self,
53 session: &Session,
54 producer: &str,
55 type_name: &str,
56 ) -> Option<TypeSchema> {
57 {
58 let sets = self.sets.lock().expect("store lock");
59 if let Some(cached) = sets.get(producer) {
60 return cached.as_ref().and_then(|s| s.get(type_name).cloned());
61 }
62 }
63 let fetched = self.fetch(session, producer).await;
64 let mut sets = self.sets.lock().expect("store lock");
65 let entry = sets.entry(producer.to_string()).or_insert(fetched);
66 entry.as_ref().and_then(|s| s.get(type_name).cloned())
67 }
68
69 async fn fetch(&self, session: &Session, producer: &str) -> Option<SchemaSet> {
70 let key = zenkey::grammar::with_base(
71 &self.base,
72 zenkey::selector::fleet_rpc(producer, &["describe"]),
73 );
74 let answers = crate::query::fleet_get(session, &self.base, &key, None, self.timeout)
75 .await
76 .ok()?;
77 for a in answers {
80 if let crate::query::Answer::Value(bytes) = a.answer {
81 let cow = bytes.to_bytes();
82 if let Ok(text) = std::str::from_utf8(&cow)
83 && let Ok(set) = SchemaSet::parse(text)
84 {
85 return Some(set);
86 }
87 }
88 }
89 None
90 }
91
92 pub fn decode(
94 &self,
95 schema: &TypeSchema,
96 encoding: &WireEncoding,
97 bytes: &[u8],
98 ) -> Result<DecodedPayload, DecodeError> {
99 self.decoders.decode(schema, encoding, bytes)
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum Rendering {
107 Typed(DecodedPayload),
109 Structural(String),
112}
113
114pub fn resolve_encoding(
117 sample_encoding: Option<&str>,
118 registry_encoding: Option<&str>,
119 bytes: &[u8],
120) -> WireEncoding {
121 if let Some(e) = sample_encoding
124 && e != "zenoh/bytes"
125 {
126 return WireEncoding::from_encoding_str(e);
127 }
128 if let Some(e) = registry_encoding {
129 return WireEncoding::from_encoding_str(e);
130 }
131 match bytes.first() {
135 Some(b'{' | b'[' | b'"') => WireEncoding::Json,
136 _ => WireEncoding::Cbor,
137 }
138}
139
140pub fn structural(bytes: &[u8]) -> String {
143 let looks_json = bytes.first().is_some_and(|b| {
144 matches!(
145 b,
146 b'{' | b'[' | b'"' | b'-' | b'0'..=b'9' | b't' | b'f' | b'n'
147 )
148 });
149 if looks_json && let Ok(v) = serde_json::from_slice::<serde_json::Value>(bytes) {
150 return serde_json::to_string(&v).unwrap_or_default();
151 }
152 if let Ok(v) = ciborium::from_reader::<ciborium::Value, _>(bytes)
153 && let Ok(text) = serde_json::to_string(&v)
154 {
155 return text;
156 }
157 match std::str::from_utf8(bytes) {
158 Ok(text) if !text.is_empty() => text.to_string(),
159 _ => format!("<{} bytes>", bytes.len()),
160 }
161}
162
163pub async fn decode_sample(
167 store: &SchemaStore,
168 session: &Session,
169 slices: &SliceSet,
170 base: &str,
171 wire_key: &str,
172 sample_encoding: Option<&str>,
173 bytes: &[u8],
174) -> (Option<String>, Rendering) {
175 use zenkey::grammar::ClassOrPlane;
176 let refined = zenkey::grammar::parse_full(base, wire_key).and_then(|parsed| {
177 let producer = match (&parsed.producer, &parsed.origin) {
178 (Some(p), _) => p.name().to_string(),
179 (None, zenkey::grammar::Origin::Service(s)) => {
180 slices.by_service_origin(s)?.name.clone()
181 }
182 _ => return None,
183 };
184 let ClassOrPlane::Class(class) = parsed.class else {
185 return None;
186 };
187 let (subject, _) = slices.refine(&producer, class.chunk(), &parsed.subject)?;
188 Some((
189 producer,
190 subject.type_name.clone(),
191 subject.encoding.clone(),
192 ))
193 });
194 let Some((producer, type_name, registry_encoding)) = refined else {
195 return (None, Rendering::Structural(structural(bytes)));
196 };
197 let encoding = resolve_encoding(sample_encoding, registry_encoding.as_deref(), bytes);
198 match store.schema_for(session, &producer, &type_name).await {
199 Some(schema) => match store.decode(&schema, &encoding, bytes) {
200 Ok(decoded) => (Some(type_name), Rendering::Typed(decoded)),
201 Err(_) => (Some(type_name), Rendering::Structural(structural(bytes))),
204 },
205 None => (Some(type_name), Rendering::Structural(structural(bytes))),
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn encoding_resolution_order() {
215 assert_eq!(
217 resolve_encoding(Some("application/json"), Some("application/cbor"), b"x"),
218 WireEncoding::Json
219 );
220 assert_eq!(
222 resolve_encoding(Some("zenoh/bytes"), Some("application/cbor"), b"{"),
223 WireEncoding::Cbor
224 );
225 assert_eq!(
227 resolve_encoding(None, None, b"{\"a\":1}"),
228 WireEncoding::Json
229 );
230 assert_eq!(resolve_encoding(None, None, &[0xa1]), WireEncoding::Cbor);
231 }
232
233 #[test]
234 fn structural_rendering_is_honest() {
235 assert_eq!(structural(b"{\"a\":1}"), "{\"a\":1}");
236 let mut cbor = Vec::new();
238 ciborium::into_writer(&serde_json::json!({"x": 1}), &mut cbor).unwrap();
239 assert!(structural(&cbor).contains("\"x\""));
240 assert_eq!(structural(&[0xff, 0xfe, 0x00]), "<3 bytes>");
241 }
242}