dvb_t2mi/payload/registry.rs
1//! Runtime payload registry — open registration of client private packet types.
2//!
3//! [`PayloadRegistry`] is a runtime-configurable dispatch engine that allows
4//! clients to register their own private T2-MI payload types alongside (or in
5//! place of) the built-ins. Registered custom parsers win over built-in
6//! dispatch when [`crate::payload::AnyPayload::dispatch_with`] is used.
7//!
8//! # Owned types only
9//!
10//! Registered types must be `'static` (i.e. owned — no borrowed slices).
11//! This is required because the parsed value is heap-allocated as a
12//! `Box<dyn PayloadObject>` whose concrete type is erased; `dyn Any`
13//! downcast demands `'static`. If your wire layout contains borrowed bytes,
14//! copy them into a `Vec<u8>` in the struct.
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use dvb_t2mi::payload::{PayloadRegistry, AnyPayload};
20//! use dvb_t2mi::traits::PayloadDef;
21//! use broadcast_common::Parse;
22//!
23//! // A registered type must be `serde::Serialize` only when the `serde`
24//! // feature is on (that is what `PayloadObject` requires there).
25//! #[derive(Debug)]
26//! #[cfg_attr(feature = "serde", derive(serde::Serialize))]
27//! struct MyPrivate { x: u8 }
28//!
29//! impl<'a> Parse<'a> for MyPrivate {
30//! type Error = dvb_t2mi::Error;
31//! fn parse(bytes: &'a [u8]) -> dvb_t2mi::Result<Self> {
32//! if bytes.is_empty() {
33//! return Err(dvb_t2mi::Error::BufferTooShort {
34//! need: 1, have: 0, what: "MyPrivate",
35//! });
36//! }
37//! Ok(Self { x: bytes[0] })
38//! }
39//! }
40//!
41//! impl<'a> PayloadDef<'a> for MyPrivate {
42//! const PACKET_TYPE: u8 = 0x40;
43//! const NAME: &'static str = "MY_PRIVATE";
44//! }
45//!
46//! let mut reg = PayloadRegistry::new();
47//! reg.register::<MyPrivate>();
48//!
49//! let bytes = [0x42u8];
50//! let result = AnyPayload::dispatch_with(
51//! ®, 0x40, &bytes,
52//! ).unwrap().unwrap();
53//! if let AnyPayload::Other { packet_type, ref value } = result {
54//! assert_eq!(packet_type, 0x40);
55//! assert_eq!(value.downcast_ref::<MyPrivate>().unwrap().x, 0x42);
56//! }
57//! ```
58
59use alloc::boxed::Box;
60use alloc::collections::BTreeMap;
61use core::any::Any;
62
63// ---------------------------------------------------------------------------
64// PayloadObject trait
65// ---------------------------------------------------------------------------
66
67/// Object-safe face of a runtime-registered payload value.
68///
69/// Registered types must be owned (`'static`) because the `dyn Any` downcast
70/// path requires it. See the [module docs][self] for details.
71///
72/// Implemented automatically via the blanket impl for any `T` satisfying the
73/// supertraits; you do not need to write this by hand.
74#[cfg(not(feature = "serde"))]
75pub trait PayloadObject: core::fmt::Debug + Any + Send + Sync {
76 /// Borrow as `&dyn Any` so the caller can downcast to the concrete type.
77 fn as_any(&self) -> &dyn Any;
78}
79
80/// Object-safe face of a runtime-registered payload value.
81///
82/// Registered types must be owned (`'static`) because the `dyn Any` downcast
83/// path requires it. See the [module docs][self] for details.
84///
85/// Implemented automatically via the blanket impl for any `T` satisfying the
86/// supertraits; you do not need to write this by hand.
87#[cfg(feature = "serde")]
88pub trait PayloadObject: core::fmt::Debug + Any + Send + Sync + erased_serde::Serialize {
89 /// Borrow as `&dyn Any` so the caller can downcast to the concrete type.
90 fn as_any(&self) -> &dyn Any;
91}
92
93// Blanket impl — no-serde arm.
94#[cfg(not(feature = "serde"))]
95impl<T> PayloadObject for T
96where
97 T: core::fmt::Debug + Any + Send + Sync,
98{
99 fn as_any(&self) -> &dyn Any {
100 self
101 }
102}
103
104// Blanket impl — serde arm.
105#[cfg(feature = "serde")]
106impl<T> PayloadObject for T
107where
108 T: core::fmt::Debug + Any + Send + Sync + serde::Serialize,
109{
110 fn as_any(&self) -> &dyn Any {
111 self
112 }
113}
114
115// Downcast helpers ON THE TRAIT OBJECT (not the blanket).
116//
117// These MUST be inherent methods on `dyn PayloadObject` rather than something
118// callable on `Box<dyn PayloadObject>` via the blanket impl. The blanket
119// `impl<T> PayloadObject for T` also covers `Box<dyn PayloadObject>` itself
120// whenever the box satisfies the bounds (it does under `--no-default-features`,
121// where the bound is just `Debug + Any + Send + Sync`). So `the_box.as_any()`
122// resolves to the *box's* impl and reports the box's `TypeId`, not the inner
123// value's — a silent downcast failure. Calling through `dyn PayloadObject`
124// (which `Box` derefs to) always hits the inner value. Always downcast via
125// these methods, never `the_box.as_any()`.
126impl dyn PayloadObject {
127 /// Downcast a registered payload to its concrete type `T`.
128 ///
129 /// Works for `Box<dyn PayloadObject>` (it derefs to the trait object) under
130 /// every feature configuration.
131 #[must_use]
132 pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
133 self.as_any().downcast_ref::<T>()
134 }
135
136 /// `true` if the registered payload's concrete type is `T`.
137 #[must_use]
138 pub fn is<T: Any>(&self) -> bool {
139 self.as_any().is::<T>()
140 }
141}
142
143// ---------------------------------------------------------------------------
144// Erased serialisation helper (serde-gated)
145// ---------------------------------------------------------------------------
146
147/// `serialize_with` helper used on [`crate::payload::AnyPayload::Other`]'s `value` field.
148///
149/// Delegates to [`erased_serde::serialize`] so the concrete type's
150/// `serde::Serialize` impl is invoked through the trait object.
151///
152/// The `&Box<T>` is required by serde's `serialize_with` codegen — the field
153/// type is `Box<dyn PayloadObject>` so serde passes `&Box<dyn PayloadObject>`.
154#[cfg(feature = "serde")]
155#[allow(clippy::borrowed_box)]
156pub(crate) fn serialize_erased<S: serde::Serializer>(
157 v: &Box<dyn PayloadObject>,
158 s: S,
159) -> Result<S::Ok, S::Error> {
160 erased_serde::serialize(&**v, s)
161}
162
163// ---------------------------------------------------------------------------
164// Internal parse closure type
165// ---------------------------------------------------------------------------
166
167/// A heap-allocated parse closure that takes raw payload bytes and returns an
168/// owned, type-erased payload value.
169pub(crate) type CustomParse =
170 Box<dyn for<'a> Fn(&'a [u8]) -> crate::Result<Box<dyn PayloadObject>> + Send + Sync>;
171
172// ---------------------------------------------------------------------------
173// PayloadRegistry
174// ---------------------------------------------------------------------------
175
176/// Runtime-configurable payload registry.
177///
178/// By default the registry has no custom parsers. Use
179/// [`register`][Self::register] to add private types, then call
180/// [`crate::payload::AnyPayload::dispatch_with`] to dispatch through the registry.
181///
182/// # Precedence (per entry)
183///
184/// 1. Custom-registered parser (packet_type in the [`register`][Self::register]
185/// map) → [`crate::payload::AnyPayload::Other`]
186/// 2. Built-in dispatch (internal [`crate::payload::AnyPayload::dispatch`]) → typed variant
187/// 3. Unknown → [`crate::payload::AnyPayload::Unknown`]
188#[derive(Default)]
189pub struct PayloadRegistry {
190 custom: BTreeMap<u8, CustomParse>,
191}
192
193impl PayloadRegistry {
194 /// Create an empty registry (built-in dispatch only).
195 #[must_use]
196 pub fn new() -> Self {
197 Self::default()
198 }
199
200 /// Register an owned custom payload type for its
201 /// [`PayloadDef::PACKET_TYPE`][crate::traits::PayloadDef::PACKET_TYPE].
202 ///
203 /// # Owned types only
204 ///
205 /// `T` must be `'static` — no borrowed slices. The registered value is
206 /// type-erased as `Box<dyn PayloadObject>`; `dyn Any` downcast requires
207 /// the concrete type to be `'static`.
208 ///
209 /// Registering a type whose `PACKET_TYPE` is already used by a built-in
210 /// **overrides** the built-in for that packet_type (custom wins precedence
211 /// in [`crate::payload::AnyPayload::dispatch_with`]).
212 ///
213 /// Re-registering the same packet_type replaces the prior custom parser
214 /// (last wins). A failing custom parse surfaces the client's
215 /// `Parse::Error` unwrapped — embed identifying context (type/ packet_type)
216 /// in your error's `what`/`reason` fields.
217 pub fn register<T>(&mut self) -> &mut Self
218 where
219 T: for<'a> crate::traits::PayloadDef<'a> + PayloadObject + 'static,
220 {
221 // Name PACKET_TYPE without a lifetime — `for<'a> PayloadDef<'a>`
222 // guarantees the const is identical for all lifetimes.
223 let packet_type = <T as crate::traits::PayloadDef<'static>>::PACKET_TYPE;
224 self.custom.insert(
225 packet_type,
226 Box::new(|b| {
227 Ok(Box::new(<T as broadcast_common::Parse>::parse(b)?) as Box<dyn PayloadObject>)
228 }),
229 );
230 self
231 }
232
233 /// Look up a custom parser for `packet_type` — `None` if not registered.
234 #[must_use]
235 pub(crate) fn lookup(&self, packet_type: u8) -> Option<&CustomParse> {
236 self.custom.get(&packet_type)
237 }
238}
239
240// ---------------------------------------------------------------------------
241// Tests
242// ---------------------------------------------------------------------------
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use broadcast_common::Parse;
248
249 // A custom owned payload type for testing, using an unused packet_type.
250 #[derive(Debug, PartialEq)]
251 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
252 struct TestPayload {
253 val: u8,
254 }
255
256 const TEST_PACKET_TYPE: u8 = 0x40;
257
258 impl<'a> Parse<'a> for TestPayload {
259 type Error = crate::Error;
260
261 fn parse(bytes: &'a [u8]) -> crate::Result<Self> {
262 if bytes.is_empty() {
263 return Err(crate::Error::BufferTooShort {
264 need: 1,
265 have: 0,
266 what: "TestPayload",
267 });
268 }
269 Ok(Self { val: bytes[0] })
270 }
271 }
272
273 impl crate::traits::PayloadDef<'_> for TestPayload {
274 const PACKET_TYPE: u8 = TEST_PACKET_TYPE;
275 const NAME: &'static str = "TEST_PAYLOAD";
276 }
277
278 #[test]
279 fn register_and_dispatch_returns_other() {
280 let mut reg = PayloadRegistry::new();
281 reg.register::<TestPayload>();
282
283 let bytes = [0x42u8];
284 let result = crate::payload::AnyPayload::dispatch_with(®, TEST_PACKET_TYPE, &bytes);
285 let parsed = result.unwrap().unwrap();
286 match parsed {
287 crate::payload::AnyPayload::Other {
288 packet_type,
289 ref value,
290 } => {
291 assert_eq!(packet_type, TEST_PACKET_TYPE);
292 let tp = value.downcast_ref::<TestPayload>().unwrap();
293 assert_eq!(tp.val, 0x42);
294 }
295 _ => panic!("expected Other, got {parsed:?}"),
296 }
297 }
298
299 #[test]
300 fn dispatch_with_falls_back_to_builtin() {
301 let reg = PayloadRegistry::new();
302 // 0x00 is Bbframe — not in registry, falls back to built-in.
303 let bytes = [0x00, 0x00, 0x00]; // minimal valid BBFrame payload
304 let result = crate::payload::AnyPayload::dispatch_with(®, 0x00, &bytes);
305 let parsed = result.unwrap().unwrap();
306 assert!(
307 matches!(parsed, crate::payload::AnyPayload::Bbframe(_)),
308 "expected Bbframe, got {parsed:?}"
309 );
310 }
311
312 #[test]
313 fn custom_overrides_builtin_packet_type() {
314 // Use a type that claims the built-in 0x00 Bbframe packet_type.
315 #[derive(Debug)]
316 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
317 struct OverridePayload;
318
319 const OVERRIDE_PT: u8 = 0x00;
320
321 impl<'a> Parse<'a> for OverridePayload {
322 type Error = crate::Error;
323 fn parse(_bytes: &'a [u8]) -> crate::Result<Self> {
324 Ok(Self)
325 }
326 }
327
328 impl crate::traits::PayloadDef<'_> for OverridePayload {
329 const PACKET_TYPE: u8 = OVERRIDE_PT;
330 const NAME: &'static str = "OVERRIDE";
331 }
332
333 let mut reg = PayloadRegistry::new();
334 reg.register::<OverridePayload>();
335
336 let bytes = [0x00, 0x00, 0x00];
337 let result = crate::payload::AnyPayload::dispatch_with(®, 0x00, &bytes);
338 let parsed = result.unwrap().unwrap();
339 assert!(
340 matches!(
341 parsed,
342 crate::payload::AnyPayload::Other {
343 packet_type: 0x00,
344 ..
345 }
346 ),
347 "expected Other override for 0x00, got {parsed:?}"
348 );
349 }
350
351 #[cfg(feature = "serde")]
352 #[test]
353 fn serde_other_round_trips_through_json() {
354 let mut reg = PayloadRegistry::new();
355 reg.register::<TestPayload>();
356
357 let bytes = [0x7Fu8];
358 let result = crate::payload::AnyPayload::dispatch_with(®, TEST_PACKET_TYPE, &bytes);
359 let parsed = result.unwrap().unwrap();
360
361 let json = serde_json::to_value(&parsed).unwrap();
362 let obj = json
363 .as_object()
364 .unwrap()
365 .get("other")
366 .expect("expected 'other' key");
367 assert_eq!(obj["packet_type"], TEST_PACKET_TYPE);
368 assert_eq!(obj["value"]["val"], 0x7F);
369 }
370}