macro_rules! declare_payloads {
(
$lt:lifetime;
$( $variant:ident = $ptype:literal => $($path:ident)::+ $(<$plt:lifetime>)? ),+ $(,)?
) => {
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
#[non_exhaustive]
pub enum AnyPayload<$lt> {
$(
#[allow(missing_docs)]
$variant($($path)::+ $(<$plt>)?),
)+
Unknown {
packet_type: u8,
body: &$lt [u8],
},
}
$(
impl<$lt> From<$($path)::+ $(<$plt>)?> for AnyPayload<$lt> {
fn from(p: $($path)::+ $(<$plt>)?) -> Self {
Self::$variant(p)
}
}
)+
impl<$lt> AnyPayload<$lt> {
pub const DISPATCHED_TYPES: &'static [u8] = &[$($ptype),+];
#[must_use]
pub fn name(&self) -> &'static str {
match self {
$(
Self::$variant(_) =>
<$($path)::+ as crate::traits::PayloadDef>::NAME,
)+
Self::Unknown { .. } => "UNKNOWN",
}
}
pub fn dispatch(
packet_type: u8,
payload_bytes: &$lt [u8],
) -> Option<crate::Result<Self>> {
use dvb_common::Parse;
match packet_type {
$(
$ptype => Some(
<$($path)::+>::parse(payload_bytes).map(Self::$variant),
),
)+
_ => None,
}
}
}
#[cfg(test)]
mod macro_drift {
#[test]
fn packet_type_literals_match_payload_def() {
use crate::traits::PayloadDef;
$(
assert_eq!(
$ptype,
<$($path)::+ as PayloadDef>::PACKET_TYPE,
concat!("PACKET_TYPE literal drift for ", stringify!($variant)),
);
assert!(
!<$($path)::+ as PayloadDef>::NAME.is_empty(),
concat!("empty NAME for ", stringify!($variant)),
);
)+
}
}
};
}
declare_payloads! {'a;
Bbframe = 0x00 => crate::payload::bbframe::BbframePayload<'a>,
AuxIq = 0x01 => crate::payload::aux_iq::AuxIqPayload<'a>,
ArbitraryCells = 0x02 => crate::payload::arbitrary_cells::ArbitraryCellsPayload<'a>,
L1Current = 0x10 => crate::payload::l1_current::L1CurrentPayload<'a>,
L1Future = 0x11 => crate::payload::l1_future::L1FuturePayload<'a>,
P2Bias = 0x12 => crate::payload::p2_bias::P2BiasPayload,
Timestamp = 0x20 => crate::payload::timestamp::T2TimestampPayload,
IndividualAddressing = 0x21 => crate::payload::individual_addressing::IndividualAddressingPayload<'a>,
FefNull = 0x30 => crate::payload::fef_null::FefNullPayload,
FefIq = 0x31 => crate::payload::fef_iq::FefIqPayload<'a>,
FefComposite = 0x32 => crate::payload::fef_composite::FefCompositePayload,
FefSubpart = 0x33 => crate::payload::fef_subpart::FefSubPartPayload<'a>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_maps_variant_to_payloaddef_name() {
let bb = AnyPayload::dispatch(0x00, &[0x00, 0x00, 0x00])
.expect("dispatched")
.expect("valid bbframe payload");
assert_eq!(bb.name(), "BBFRAME");
let unknown = AnyPayload::Unknown {
packet_type: 0x7F,
body: &[],
};
assert_eq!(unknown.name(), "UNKNOWN");
}
#[test]
fn every_dispatched_type_routes_non_unknown() {
let bbframe_bytes: &[u8] = &[0x00, 0x00, 0x00];
let aux_iq_bytes: &[u8] = &[0x00, 0x10, 0x00];
let arb_cells_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
let l1_current_bytes: &[u8] = &[0x00, 0x00];
let l1_future_bytes: &[u8] = &[0x00, 0x00];
let p2_bias_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00];
let timestamp_bytes: &[u8] = &[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let indiv_addr_bytes: &[u8] = &[0x00, 0x00];
let fef_null_bytes: &[u8] = &[0x00, 0x00, 0x00];
let fef_iq_bytes: &[u8] = &[0x00, 0x00, 0x00];
let fef_composite_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
let fef_subpart_bytes: &[u8] = &[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
];
let fixtures: &[(u8, &[u8])] = &[
(0x00, bbframe_bytes),
(0x01, aux_iq_bytes),
(0x02, arb_cells_bytes),
(0x10, l1_current_bytes),
(0x11, l1_future_bytes),
(0x12, p2_bias_bytes),
(0x20, timestamp_bytes),
(0x21, indiv_addr_bytes),
(0x30, fef_null_bytes),
(0x31, fef_iq_bytes),
(0x32, fef_composite_bytes),
(0x33, fef_subpart_bytes),
];
for &(pt, bytes) in fixtures {
let result = AnyPayload::dispatch(pt, bytes);
assert!(result.is_some(), "0x{pt:02x} returned None from dispatch");
let parsed = result.unwrap();
assert!(
parsed.is_ok(),
"0x{pt:02x} dispatch parse failed: {:?}",
parsed.unwrap_err()
);
assert!(
!matches!(parsed.unwrap(), AnyPayload::Unknown { .. }),
"0x{pt:02x} was dispatched to Unknown"
);
}
}
#[test]
fn dispatched_types_count_is_twelve() {
assert_eq!(AnyPayload::DISPATCHED_TYPES.len(), 12);
}
#[test]
fn dispatched_types_contains_all_defined_packet_types() {
let expected = [
0x00u8, 0x01, 0x02, 0x10, 0x11, 0x12, 0x20, 0x21, 0x30, 0x31, 0x32, 0x33,
];
for pt in expected {
assert!(
AnyPayload::DISPATCHED_TYPES.contains(&pt),
"0x{pt:02x} missing from DISPATCHED_TYPES"
);
}
}
#[test]
fn undispatched_packet_type_returns_none() {
assert!(AnyPayload::dispatch(0x22, &[]).is_none());
assert!(AnyPayload::dispatch(0xFF, &[]).is_none());
}
#[test]
fn from_bbframe_payload_into_any_payload() {
use crate::payload::bbframe::BbframePayload;
let p = BbframePayload {
frame_idx: 1,
plp_id: 2,
intl_frame_start: false,
bbframe: &[],
};
let any = AnyPayload::from(p);
assert!(matches!(any, AnyPayload::Bbframe(_)));
}
#[test]
fn from_fef_null_payload_into_any_payload() {
use crate::payload::fef_null::{FefNullPayload, S1Field};
let p = FefNullPayload {
fef_idx: 0,
s1_field: S1Field::V0,
s2_field: 0,
};
let any = AnyPayload::from(p);
assert!(matches!(any, AnyPayload::FefNull(_)));
}
#[cfg(feature = "serde")]
#[test]
fn bbframe_serializes_as_camel_case_external_tag() {
use crate::payload::bbframe::BbframePayload;
let p = BbframePayload {
frame_idx: 0x42,
plp_id: 0x05,
intl_frame_start: true,
bbframe: &[],
};
let any = AnyPayload::Bbframe(p);
let json = serde_json::to_value(&any).unwrap();
assert!(
json.get("bbframe").is_some(),
"expected camelCase 'bbframe' key, got: {json}"
);
assert_eq!(json["bbframe"]["frame_idx"], 0x42);
}
#[cfg(feature = "serde")]
#[test]
fn unknown_serializes_with_packet_type_and_body() {
let any = AnyPayload::Unknown {
packet_type: 0x22,
body: &[0xDE, 0xAD],
};
let json = serde_json::to_value(&any).unwrap();
assert!(
json.get("unknown").is_some(),
"expected 'unknown' key, got: {json}"
);
assert_eq!(json["unknown"]["packet_type"], 0x22);
}
}