Skip to main content

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}