scte35_splice/commands/any.rs
1//! Unified command dispatch: [`AnyCommand`].
2//!
3//! [`AnyCommand`] is generated from a single declarative list
4//! (`declare_commands!`) — one line per `splice_command_type` (§9.6.1,
5//! Table 7). The list is the single source of truth: it produces the enum, the
6//! `From<T>` conversions, the type → parser dispatcher, and a drift test that
7//! pins each command-type literal to the type's
8//! [`CommandDef::COMMAND_TYPE`](crate::traits::CommandDef::COMMAND_TYPE).
9//!
10//! A `splice_command_type` with no typed implementation (the reserved values)
11//! falls through to [`AnyCommand::Unknown`], which keeps the raw command body
12//! so a section round-trips byte-for-byte.
13
14use crate::error::Result;
15
16/// Declares [`AnyCommand`] + its dispatcher from one command-type list.
17macro_rules! declare_commands {
18 (
19 $lt:lifetime;
20 $( $variant:ident = $ct:literal => $($path:ident)::+ $(<$plt:lifetime>)? ),+ $(,)?
21 ) => {
22 /// Every crate-implemented splice command, plus an `Unknown`
23 /// fallthrough that preserves the raw command body for lossless
24 /// round-trips.
25 ///
26 /// serde uses external tagging with camelCase variant keys.
27 #[derive(Debug, Clone, PartialEq, Eq)]
28 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
29 #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
30 #[non_exhaustive]
31 pub enum AnyCommand<$lt> {
32 $(
33 #[allow(missing_docs)]
34 $variant($($path)::+ $(<$plt>)?),
35 )+
36 /// A `splice_command_type` with no typed implementation; `body` is
37 /// the raw command bytes (`splice_command_length` bytes).
38 Unknown {
39 /// The raw `splice_command_type` byte.
40 command_type: u8,
41 /// The raw command body bytes.
42 body: &$lt [u8],
43 },
44 }
45
46 $(
47 impl<$lt> From<$($path)::+ $(<$plt>)?> for AnyCommand<$lt> {
48 fn from(c: $($path)::+ $(<$plt>)?) -> Self {
49 Self::$variant(c)
50 }
51 }
52 )+
53
54 impl<$lt> AnyCommand<$lt> {
55 /// Every `splice_command_type` the generated dispatcher routes
56 /// (excludes [`AnyCommand::Unknown`]).
57 pub const DISPATCHED_TYPES: &'static [u8] = &[$($ct),+];
58
59 /// Diagnostic name of the contained command — the type's
60 /// [`CommandDef::NAME`](crate::traits::CommandDef::NAME)
61 /// (`"SPLICE_INSERT"`, `"TIME_SIGNAL"`, …); `"UNKNOWN"` for
62 /// [`AnyCommand::Unknown`].
63 #[must_use]
64 pub fn name(&self) -> &'static str {
65 match self {
66 $(
67 Self::$variant(_) =>
68 <$($path)::+ as crate::traits::CommandDef>::NAME,
69 )+
70 Self::Unknown { .. } => "UNKNOWN",
71 }
72 }
73
74 /// The wire `splice_command_type` byte for this command.
75 #[must_use]
76 pub fn command_type(&self) -> u8 {
77 match self {
78 $(
79 Self::$variant(_) =>
80 <$($path)::+ as crate::traits::CommandDef>::COMMAND_TYPE,
81 )+
82 Self::Unknown { command_type, .. } => *command_type,
83 }
84 }
85
86 /// Parse a command `body` by its `splice_command_type`. Reserved /
87 /// unimplemented types yield [`AnyCommand::Unknown`].
88 pub fn dispatch(command_type: u8, body: &$lt [u8]) -> Result<Self> {
89 use broadcast_common::Parse;
90 match command_type {
91 $(
92 $ct => <$($path)::+>::parse(body).map(Self::$variant),
93 )+
94 _ => Ok(Self::Unknown { command_type, body }),
95 }
96 }
97
98 /// Number of bytes [`serialize_body_into`](Self::serialize_body_into)
99 /// will write (the `splice_command_length`).
100 #[must_use]
101 pub fn body_len(&self) -> usize {
102 use broadcast_common::Serialize;
103 match self {
104 $(
105 Self::$variant(c) => c.serialized_len(),
106 )+
107 Self::Unknown { body, .. } => body.len(),
108 }
109 }
110
111 /// Serialize just the command body (no type byte) into `buf`.
112 pub fn serialize_body_into(&self, buf: &mut [u8]) -> Result<usize> {
113 use broadcast_common::Serialize;
114 match self {
115 $(
116 Self::$variant(c) => c.serialize_into(buf),
117 )+
118 Self::Unknown { body, .. } => {
119 if buf.len() < body.len() {
120 return Err(crate::error::Error::OutputBufferTooSmall {
121 need: body.len(),
122 have: buf.len(),
123 });
124 }
125 buf[..body.len()].copy_from_slice(body);
126 Ok(body.len())
127 }
128 }
129 }
130 }
131
132 #[cfg(test)]
133 mod macro_drift {
134 #[test]
135 fn command_type_literals_match_command_def() {
136 use crate::traits::CommandDef;
137 $(
138 assert_eq!(
139 $ct,
140 <$($path)::+ as CommandDef>::COMMAND_TYPE,
141 concat!("command_type literal drift for ", stringify!($variant)),
142 );
143 assert!(
144 !<$($path)::+ as CommandDef>::NAME.is_empty(),
145 concat!("empty NAME for ", stringify!($variant)),
146 );
147 )+
148 }
149 }
150 };
151}
152
153declare_commands! {'a;
154 SpliceNull = 0x00 => crate::commands::splice_null::SpliceNull,
155 SpliceSchedule = 0x04 => crate::commands::splice_schedule::SpliceSchedule,
156 SpliceInsert = 0x05 => crate::commands::splice_insert::SpliceInsert,
157 TimeSignal = 0x06 => crate::commands::time_signal::TimeSignal,
158 BandwidthReservation = 0x07 => crate::commands::bandwidth_reservation::BandwidthReservation,
159 PrivateCommand = 0xFF => crate::commands::private_command::PrivateCommand<'a>,
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn unknown_command_type_round_trips_body() {
168 let body = [0xDE, 0xAD, 0xBE, 0xEF];
169 let cmd = AnyCommand::dispatch(0x03, &body).unwrap();
170 assert!(matches!(
171 cmd,
172 AnyCommand::Unknown {
173 command_type: 0x03,
174 ..
175 }
176 ));
177 assert_eq!(cmd.body_len(), 4);
178 assert_eq!(cmd.command_type(), 0x03);
179 assert_eq!(cmd.name(), "UNKNOWN");
180 let mut buf = vec![0u8; cmd.body_len()];
181 cmd.serialize_body_into(&mut buf).unwrap();
182 assert_eq!(buf, body);
183 }
184
185 #[test]
186 fn dispatch_splice_null() {
187 let cmd = AnyCommand::dispatch(0x00, &[]).unwrap();
188 assert_eq!(cmd.name(), "SPLICE_NULL");
189 assert_eq!(cmd.command_type(), 0x00);
190 assert_eq!(cmd.body_len(), 0);
191 }
192}