mediaframe/serde_impls/mod.rs
1//! Centralised `serde` implementations for the descriptor enums
2//! (`feature = "serde"`).
3//!
4//! # Two laws, and the second one has legs
5//!
6//! **An open vocabulary is always its slug.** **A closed one splits on
7//! the format**: its name where a human will read it, its code where
8//! only a machine will. `Serializer::is_human_readable()` is what says
9//! which, and the split is per-format rather than per-type — the same
10//! value is `"native"` in JSON and a varint in postcard, and neither is
11//! a fallback for the other.
12//!
13//! The reason the two laws differ is the escape arm, not taste. An open
14//! vocabulary's `Other(SmolStr)` holds a name and *only* a name: there
15//! is no code to fall back to for a value this build has never heard
16//! of, so a numeric leg could not carry one and the slug is the only
17//! honest wire at either end. A closed vocabulary has no such value —
18//! every member has both spellings, so the format gets to choose, and a
19//! binary format has no reason to pay for a string it cannot read
20//! anyway.
21//!
22//! ## The open law — always the slug
23//!
24//! - **Every open vocabulary enum** — codecs, formats, the colour enums,
25//! the pixel format, the frame coded enums — serializes as its
26//! canonical `as_str()` slug: `VideoCodec::H264` ⇄ `"h264"`,
27//! `color::Matrix::Bt709` ⇄ `"bt709"`, `Other("x265")` ⇄ `"x265"` (no
28//! `{"Other": …}` wrapper). One extension idiom, one wire shape, every
29//! format. Round-trip total wherever the `Other(SmolStr)` arm exists
30//! (the `alloc` tier); at the no-alloc tier the same enums are closed,
31//! so an unrecognised slug is a serde error rather than a
32//! silently-invented value. Deserialization goes through the type's
33//! `FromStr`, so it also reads the documented FFmpeg synonyms
34//! (`"gray"` → `PixelFormat::Gray8`, `"unknown"` →
35//! `color::Matrix::Unspecified`); serialization stays canonical, so a
36//! synonym read off the wire is written back in the canonical
37//! spelling.
38//! - **`TrackDisposition`** is outside both laws: it is a bit set, not a
39//! name vocabulary, so it serializes as its `u32` bits. The number
40//! *is* the value and there is no name to spell — in any format.
41//!
42//! ## The closed law — the slug leg and the code leg
43//!
44//! **Strictly-closed coded enums (no `Other` arm)** —
45//! [`crate::audio::BitRateMode`], [`crate::audio::ChannelOrder`] — take
46//! both legs, and **both legs are strict**:
47//!
48//! | leg | shape | read side |
49//! |---|---|---|
50//! | `is_human_readable()` | the `as_str()` slug (`"cbr"`, `"native"`) | the type's `FromStr`; an unrecognised **name** is a serde error, and a *number* is refused outright — it is not a name |
51//! | binary | the `to_u32()` code | `try_from_u32`; an out-of-range **code** is a serde error |
52//!
53//! Strict on both legs means the same thing on both: an input this
54//! vocabulary cannot name is *refused*, never collapsed onto the default
55//! variant the way `from_u32` would collapse it (`BitRateMode::from_u32(999)
56//! == Cbr`, `ChannelOrder::from_u32(999) == Unspecified`). A corrupt or
57//! out-of-range value must fail loudly rather than arrive looking like
58//! valid data. The slug leg still folds ASCII case, because that is the
59//! whole of the crate's folding and `"CBR"` is the same *name* as
60//! `"cbr"` — folding a spelling is not inventing a value.
61//!
62//! [`crate::subtitle::TrackOrigin`] left this group in 0.5.0 when it
63//! gained an `Other` arm: an open vocabulary has no closed code space to
64//! police, and no code for its escape to carry, so it moved to the open
65//! law above and stays there under both formats.
66//!
67//! `ChannelOrder`'s code space really is closed, which is what puts it
68//! here: it mirrors FFmpeg's `AVChannelOrder`, four members with no
69//! vendor range, so every integer outside `0..=3` is a corrupt read
70//! rather than a name this build has not heard of.
71//!
72//! The plain data structs (`color::Info`, `frame::Dimensions`,
73//! `audio::Tags`, `audio::ChannelSpec`,
74//! `audio::ChannelLayoutDescription`, …) derive serde at their
75//! definition site; the
76//! validated structs (`capture::GeoLocation`, `audio::Fingerprint`,
77//! `audio::CoverArt`, `frame::WhiteBalance`,
78//! `frame::ColorCorrectionMatrix`) route deserialize through their
79//! checking constructors there too. The `lang` household carries bespoke
80//! canonical-text impls for all four of its types in its own module.
81
82/// Implements `Serialize` / `Deserialize` for an *open* enum via its
83/// canonical string slug (`as_str()` to serialize, [`FromStr`] to parse).
84/// The `FromStr` impl is total (`Err = Infallible`) — unknown slugs ride
85/// the enum's `Other` arm — but the deserializer surfaces any error as a
86/// serde error for forward-compatibility.
87///
88/// [`FromStr`]: core::str::FromStr
89macro_rules! serde_via_str {
90 ($t:path) => {
91 impl serde::Serialize for $t {
92 #[inline]
93 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
94 ser.serialize_str(self.as_str())
95 }
96 }
97
98 impl<'de> serde::Deserialize<'de> for $t {
99 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
100 struct V;
101 impl serde::de::Visitor<'_> for V {
102 type Value = $t;
103 fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104 f.write_str(concat!("a ", stringify!($t), " slug string"))
105 }
106 #[inline]
107 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
108 v.parse::<$t>().map_err(serde::de::Error::custom)
109 }
110 }
111 de.deserialize_str(V)
112 }
113 }
114 };
115}
116
117/// Implements `Serialize` / `Deserialize` via a `u32` whose every value is
118/// meaningful wire data — the bit-set case, where the number *is* the
119/// value and there is no name to spell. `TrackDisposition` is the only
120/// such type; name vocabularies use [`serde_via_str!`] instead.
121macro_rules! serde_via_code {
122 ($t:path) => {
123 impl serde::Serialize for $t {
124 #[inline]
125 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
126 ser.serialize_u32(self.to_u32())
127 }
128 }
129
130 impl<'de> serde::Deserialize<'de> for $t {
131 #[inline]
132 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
133 Ok(<$t>::from_u32(<u32 as serde::Deserialize>::deserialize(
134 de,
135 )?))
136 }
137 }
138 };
139}
140
141/// Implements `Serialize` / `Deserialize` for a **strictly-closed**
142/// FFmpeg-coded enum — one with no escape arm at all — with a leg per
143/// format: the `as_str()` slug where `is_human_readable()`, the
144/// `to_u32()` code where it is not.
145///
146/// Both legs are strict, and strict means the same thing on each: an
147/// input this vocabulary cannot name is **refused**, never collapsed
148/// onto the default variant the way `from_u32` would collapse it. The
149/// slug leg refuses an unrecognised name through the type's own
150/// `FromStr`; the code leg refuses an out-of-range code through
151/// `try_from_u32`.
152///
153/// Two properties of the slug leg are load-bearing and easy to lose:
154///
155/// * **A number is not a name.** The visitor implements `visit_str` and
156/// nothing else, so a JSON `1` reaches serde's default `visit_u64` and
157/// comes back as an `invalid type` error rather than being read as a
158/// code. The legs are alternatives, not a chain of fallbacks — a
159/// human-readable document that carries a bare integer here is
160/// malformed, not merely terse.
161/// * **Case still folds.** `FromStr` goes through the crate's one ASCII
162/// folding gate, so `"CBR"` and `"cbr"` are one value. Folding a
163/// *spelling* is not inventing a *value*, which is the line strictness
164/// is drawn on.
165///
166/// An open vocabulary does **not** get this treatment — see
167/// [`serde_via_str!`] and the two laws in the module docs. Its
168/// `Other(SmolStr)` holds a name with no code behind it, so a numeric
169/// leg would have nothing to write.
170// Both invocations are heap-tier — gated on
171// `any(feature = "std", feature = "alloc")`. Under bare `--features serde`
172// (no-alloc tier) they are cfg'd out and the macro is unused; the `allow`
173// silences the resulting `unused_macros` lint, exactly as for `serde_via_str!`.
174#[allow(unused_macros)]
175macro_rules! serde_via_slug_or_code {
176 ($t:path) => {
177 impl serde::Serialize for $t {
178 #[inline]
179 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
180 if ser.is_human_readable() {
181 ser.serialize_str(self.as_str())
182 } else {
183 ser.serialize_u32(self.to_u32())
184 }
185 }
186 }
187
188 impl<'de> serde::Deserialize<'de> for $t {
189 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
190 if de.is_human_readable() {
191 struct V;
192 impl serde::de::Visitor<'_> for V {
193 type Value = $t;
194 fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
195 f.write_str(concat!("a ", stringify!($t), " slug string"))
196 }
197 #[inline]
198 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
199 v.parse::<$t>().map_err(serde::de::Error::custom)
200 }
201 }
202 de.deserialize_str(V)
203 } else {
204 let v = <u32 as serde::Deserialize>::deserialize(de)?;
205 <$t>::try_from_u32(v).ok_or_else(|| {
206 serde::de::Error::custom(::std::format!(
207 "{}: unknown wire code {}",
208 stringify!($t),
209 v
210 ))
211 })
212 }
213 }
214 }
215 };
216}
217
218// ── The bit set: a number is its only faithful spelling ──
219serde_via_code!(crate::disposition::TrackDisposition);
220
221// ── Name vocabularies available at every capability tier ──
222// Open at the `alloc` tier (an unrecognised slug rides `Other`), closed at
223// the no-alloc tier (it is a serde error) — one wire shape either way.
224serde_via_str!(crate::color::Matrix);
225serde_via_str!(crate::color::Primaries);
226serde_via_str!(crate::color::Transfer);
227serde_via_str!(crate::color::DynamicRange);
228serde_via_str!(crate::color::ChromaLocation);
229serde_via_str!(crate::color::DcpTargetGamut);
230serde_via_str!(crate::pixel_format::PixelFormat);
231serde_via_str!(crate::frame::Rotation);
232serde_via_str!(crate::frame::FieldOrder);
233serde_via_str!(crate::frame::StereoMode);
234
235// ── The RAW / bayer vocabularies (behind the `bayer` feature) ──
236// Closed: they name sensor layouts and demosaic algorithms, not an open
237// space a backend extends, so an unrecognised slug is a serde error.
238// `WhiteBalance` / `ColorCorrectionMatrix` are float structs and carry
239// their own validating impls at their definition site.
240#[cfg(feature = "bayer")]
241serde_via_str!(crate::frame::BayerPattern);
242#[cfg(feature = "bayer")]
243serde_via_str!(crate::frame::BayerDemosaic);
244#[cfg(feature = "bayer")]
245serde_via_str!(crate::frame::WbChannel);
246
247// ── Name vocabularies that need the allocator for their own payloads ──
248#[cfg(any(feature = "std", feature = "alloc"))]
249serde_via_str!(crate::codec::VideoCodec);
250#[cfg(any(feature = "std", feature = "alloc"))]
251serde_via_str!(crate::codec::AudioCodec);
252#[cfg(any(feature = "std", feature = "alloc"))]
253serde_via_str!(crate::codec::SubtitleCodec);
254#[cfg(any(feature = "std", feature = "alloc"))]
255serde_via_str!(crate::codec::DataCodec);
256#[cfg(any(feature = "std", feature = "alloc"))]
257serde_via_str!(crate::codec::AttachmentCodec);
258#[cfg(any(feature = "std", feature = "alloc"))]
259serde_via_str!(crate::container::Format);
260#[cfg(any(feature = "std", feature = "alloc"))]
261serde_via_str!(crate::image::Format);
262#[cfg(any(feature = "std", feature = "alloc"))]
263serde_via_str!(crate::subtitle::Format);
264#[cfg(any(feature = "std", feature = "alloc"))]
265serde_via_str!(crate::subtitle::TrackOrigin);
266#[cfg(any(feature = "std", feature = "alloc"))]
267serde_via_str!(crate::audio::ChannelLayout);
268#[cfg(any(feature = "std", feature = "alloc"))]
269serde_via_str!(crate::audio::SampleFormat);
270#[cfg(any(feature = "std", feature = "alloc"))]
271serde_via_str!(crate::audio::ContainerFormat);
272
273// ── Strictly-closed coded enums (no `Unknown` escape) ──
274// Use `serde_via_slug_or_code!` — the slug where a human reads it, the
275// code where only a machine does, and both legs strict: an unrecognised
276// name or an out-of-range code is a serde error, never canonicalised to
277// the default (which `from_u32` would do for
278// `BitRateMode::from_u32(999) == Cbr` and
279// `ChannelOrder::from_u32(999) == Unspecified`).
280//
281// No exceptions here: every member of this group takes both legs. A
282// closed vocabulary pinned to one shape would be exactly the asymmetry
283// the two-law split exists to remove.
284#[cfg(any(feature = "std", feature = "alloc"))]
285serde_via_slug_or_code!(crate::audio::BitRateMode);
286#[cfg(any(feature = "std", feature = "alloc"))]
287serde_via_slug_or_code!(crate::audio::ChannelOrder);
288
289#[cfg(all(test, feature = "std"))]
290mod tests;