Skip to main content

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//!     &reg, 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<'a> crate::traits::PayloadDef<'a> 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(&reg, 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(&reg, 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<'a> crate::traits::PayloadDef<'a> 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(&reg, 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(&reg, 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}