1use std::collections::BTreeMap;
11
12use crate::bus::monitor::{SampleView, StampProvenance};
13use crate::model::facts::{KeyFacts, KeyShape, Registration};
14use crate::report::{AnsweredBy, Holder, RegistrationWire, StamperWire};
15
16pub type Replier = Option<zenoh::config::ZenohId>;
18
19pub fn fold_latest(
30 values: Vec<(SampleView, Replier)>,
31) -> (BTreeMap<String, (SampleView, Replier)>, u64) {
32 let mut kept: BTreeMap<String, (SampleView, Replier)> = BTreeMap::new();
33 let mut superseded = 0u64;
34 for (view, replier) in values {
35 match kept.get(&view.key) {
36 None => {
37 kept.insert(view.key.clone(), (view, replier));
38 }
39 Some((cur, _)) => {
40 let newer = match (cur.timestamp, view.timestamp) {
41 (Some(a), Some(b)) => b > a,
42 (None, Some(_)) => true,
43 _ => false,
44 };
45 superseded += 1;
46 if newer {
47 kept.insert(view.key.clone(), (view, replier));
48 }
49 }
50 }
51 }
52 (kept, superseded)
53}
54
55pub fn holder_of(
63 base: &str,
64 key: &str,
65 view: &SampleView,
66 replier: Replier,
67 roster: Option<&BTreeMap<String, Vec<String>>>,
68) -> Holder {
69 let Some(roster) = roster else {
70 return Holder::Unattributed {
71 reason: "roster not asked".into(),
72 };
73 };
74 let facts = KeyFacts::project(base, key);
75 let origin = match &facts.shape {
76 KeyShape::V1(f) => f.origin.clone(),
77 KeyShape::NotUnderBase => {
78 return Holder::Unattributed {
79 reason: "the key is not under the stated base, so it names no origin here".into(),
80 };
81 }
82 KeyShape::Unparsed { reason } => {
83 return Holder::Unattributed {
84 reason: format!("the key names no origin: {reason}"),
85 };
86 }
87 };
88 if !roster.contains_key(&origin) {
89 return Holder::StorageOnly { origin };
90 }
91 Holder::Live {
92 origin,
93 answered_by: answered_by(view, replier),
94 }
95}
96
97fn answered_by(view: &SampleView, replier: Replier) -> AnsweredBy {
101 let stamper = match view.stamped_by {
102 Some(StampProvenance::SelfStamped) => {
103 view.source.map(|s| zenoh::time::TimestampId::from(s.zid))
104 }
105 Some(StampProvenance::Foreign { stamper })
106 | Some(StampProvenance::Unattributable { stamper }) => Some(stamper),
107 None => None,
108 };
109 match (stamper, replier) {
110 (Some(s), Some(r)) if s == zenoh::time::TimestampId::from(r) => AnsweredBy::Stamper,
111 (Some(_), Some(_)) => AnsweredBy::Other,
112 _ => AnsweredBy::Unknown,
113 }
114}
115
116pub fn registration_of(facts: &KeyFacts) -> RegistrationWire {
120 match &facts.shape {
121 KeyShape::NotUnderBase => RegistrationWire::NotUnderBase,
122 KeyShape::Unparsed { .. } => RegistrationWire::NotV1,
123 KeyShape::V1(_) => match &facts.registration {
124 Registration::Unknown => RegistrationWire::RegistryNotLoaded,
125 Registration::NoSliceForProducer => RegistrationWire::NoSliceForProducer,
126 Registration::Unregistered => RegistrationWire::Unregistered,
127 Registration::Registered(_) => RegistrationWire::Registered,
128 Registration::NotApplicable => RegistrationWire::NotADataClass,
129 },
130 }
131}
132
133#[cfg(feature = "decode")]
136pub fn verdict_of(verdict: &zenkey::schema::validate::Verdict) -> crate::report::VerdictWire {
137 use crate::report::VerdictWire;
138 use zenkey::schema::validate::{NotValidated, Verdict};
139 match verdict {
140 Verdict::Valid => VerdictWire::Valid,
141 Verdict::Invalid(violations) => VerdictWire::Invalid {
142 violations: violations.clone(),
143 },
144 Verdict::NotValidated(reason) => VerdictWire::NotValidated {
145 reason: match reason {
146 NotValidated::NoSchema => "no_schema",
147 NotValidated::NoRegistry => "no_registry",
148 NotValidated::FeatureOff => "feature_off",
149 NotValidated::KindUnsupported => "kind_unsupported",
150 NotValidated::Undecodable => "undecodable",
151 NotValidated::BadSchema => "bad_schema",
152 }
153 .into(),
154 },
155 }
156}
157
158pub fn stamper_of(provenance: &StampProvenance) -> StamperWire {
160 match provenance {
161 StampProvenance::SelfStamped => StamperWire::SelfStamped,
162 StampProvenance::Foreign { stamper } => StamperWire::Foreign {
163 id: stamper.to_string(),
164 },
165 StampProvenance::Unattributable { stamper } => StamperWire::Unattributable {
166 id: stamper.to_string(),
167 },
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use std::time::{Duration, Instant};
175
176 fn stamp(secs: u64, id: zenoh::time::TimestampId) -> zenoh::time::Timestamp {
177 zenoh::time::Timestamp::new(zenoh::time::NTP64::from(Duration::from_secs(secs)), id)
178 }
179
180 fn view(key: &str, payload: &[u8], timestamp: Option<zenoh::time::Timestamp>) -> SampleView {
181 SampleView {
182 key: key.to_string(),
183 payload: zenoh::bytes::ZBytes::from(payload.to_vec()),
184 encoding: String::new(),
185 kind: zenoh::sample::SampleKind::Put,
186 stamped_by: timestamp.map(|t| StampProvenance::Unattributable {
187 stamper: *t.get_id(),
188 }),
189 timestamp,
190 attachment: None,
191 priority: zenoh::qos::Priority::DEFAULT,
192 congestion_control: zenoh::qos::CongestionControl::DEFAULT,
193 reliability: zenoh::qos::Reliability::DEFAULT,
194 express: false,
195 source: None,
196 received: Instant::now(),
197 }
198 }
199
200 const KEY: &str = "v1/h-3fa9c2d41b7e/state/sysinfo/health";
201
202 #[test]
205 fn the_fold_is_last_writer_wins_and_counts_what_lost() {
206 let id = zenoh::time::TimestampId::rand();
207 let (kept, superseded) = fold_latest(vec![
208 (view(KEY, b"old", Some(stamp(10, id))), None),
209 (view(KEY, b"new", Some(stamp(20, id))), None),
210 (view(KEY, b"stale", Some(stamp(5, id))), None),
211 (view(KEY, b"unstamped", None), None),
212 ]);
213 assert_eq!(superseded, 3);
214 assert_eq!(kept[KEY].0.payload.to_bytes().as_ref(), b"new");
215
216 let (kept, superseded) = fold_latest(vec![
217 (view(KEY, b"first", None), None),
218 (view(KEY, b"second", None), None),
219 ]);
220 assert_eq!(superseded, 1);
221 assert_eq!(
222 kept[KEY].0.payload.to_bytes().as_ref(),
223 b"first",
224 "two unstamped values cannot be reconciled; the first seen stands"
225 );
226
227 let (kept, superseded) = fold_latest(vec![
228 (view(KEY, b"unstamped", None), None),
229 (view(KEY, b"stamped", Some(stamp(1, id))), None),
230 ]);
231 assert_eq!(superseded, 1);
232 assert_eq!(kept[KEY].0.payload.to_bytes().as_ref(), b"stamped");
233 }
234
235 #[test]
238 fn the_holder_is_evidence_at_every_rung() {
239 let v = view(KEY, b"{}", None);
240 assert_eq!(
241 holder_of("", KEY, &v, None, None),
242 Holder::Unattributed {
243 reason: "roster not asked".into()
244 }
245 );
246 let roster: BTreeMap<String, Vec<String>> = BTreeMap::new();
247 assert!(matches!(
248 holder_of("", "not/a/v1/key", &v, None, Some(&roster)),
249 Holder::Unattributed { .. }
250 ));
251 assert!(matches!(
252 holder_of("acme", KEY, &v, None, Some(&roster)),
253 Holder::Unattributed { reason } if reason.contains("not under the stated base")
254 ));
255 assert_eq!(
256 holder_of("", KEY, &v, None, Some(&roster)),
257 Holder::StorageOnly {
258 origin: "h-3fa9c2d41b7e".into()
259 }
260 );
261
262 let mut roster = roster;
263 roster.insert("h-3fa9c2d41b7e".into(), vec!["sysinfo".into()]);
264 assert_eq!(
265 holder_of("", KEY, &v, None, Some(&roster)),
266 Holder::Live {
267 origin: "h-3fa9c2d41b7e".into(),
268 answered_by: AnsweredBy::Unknown,
269 },
270 "unstamped and no replier: nothing to compare (O4)"
271 );
272
273 let stamper = zenoh::config::ZenohId::default();
274 let stamped = view(KEY, b"{}", Some(stamp(1, stamper.into())));
275 assert_eq!(
276 holder_of("", KEY, &stamped, Some(stamper), Some(&roster)),
277 Holder::Live {
278 origin: "h-3fa9c2d41b7e".into(),
279 answered_by: AnsweredBy::Stamper,
280 }
281 );
282 let other = view(KEY, b"{}", Some(stamp(1, zenoh::time::TimestampId::rand())));
283 assert_eq!(
284 holder_of("", KEY, &other, Some(stamper), Some(&roster)),
285 Holder::Live {
286 origin: "h-3fa9c2d41b7e".into(),
287 answered_by: AnsweredBy::Other,
288 }
289 );
290 assert_eq!(
291 holder_of("", KEY, &stamped, None, Some(&roster)),
292 Holder::Live {
293 origin: "h-3fa9c2d41b7e".into(),
294 answered_by: AnsweredBy::Unknown,
295 },
296 "a stamp with no replier id is unknown, not other"
297 );
298 }
299
300 #[test]
303 fn registration_keeps_not_loaded_apart_from_unregistered() {
304 assert_eq!(
305 registration_of(&KeyFacts::project("", KEY)),
306 RegistrationWire::RegistryNotLoaded
307 );
308 assert_eq!(
309 registration_of(&KeyFacts::project("acme", KEY)),
310 RegistrationWire::NotUnderBase
311 );
312 assert_eq!(
313 registration_of(&KeyFacts::project("", "plain/zenoh/key")),
314 RegistrationWire::NotV1
315 );
316 assert_eq!(
317 registration_of(&KeyFacts::project(
318 "",
319 "v1/h-3fa9c2d41b7e/@rpc/sysinfo/ping"
320 )),
321 RegistrationWire::NotADataClass
322 );
323 }
324}