#[allow(unused_macros)]
macro_rules! serde_via_str {
($t:path) => {
impl serde::Serialize for $t {
#[inline]
fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(self.as_str())
}
}
impl<'de> serde::Deserialize<'de> for $t {
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
struct V;
impl serde::de::Visitor<'_> for V {
type Value = $t;
fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(concat!("a ", stringify!($t), " slug string"))
}
#[inline]
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
v.parse::<$t>().map_err(serde::de::Error::custom)
}
}
de.deserialize_str(V)
}
}
};
}
macro_rules! serde_via_code {
($t:path) => {
impl serde::Serialize for $t {
#[inline]
fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_u32(self.to_u32())
}
}
impl<'de> serde::Deserialize<'de> for $t {
#[inline]
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
Ok(<$t>::from_u32(<u32 as serde::Deserialize>::deserialize(
de,
)?))
}
}
};
}
#[allow(unused_macros)]
macro_rules! serde_via_code_strict {
($t:path) => {
impl serde::Serialize for $t {
#[inline]
fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_u32(self.to_u32())
}
}
impl<'de> serde::Deserialize<'de> for $t {
#[inline]
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
let v = <u32 as serde::Deserialize>::deserialize(de)?;
<$t>::try_from_u32(v).ok_or_else(|| {
serde::de::Error::custom(::std::format!(
"{}: unknown wire code {}",
stringify!($t),
v
))
})
}
}
};
}
#[cfg(any(feature = "std", feature = "alloc"))]
const _: () = {
use crate::audio::SampleFormat;
use core::{fmt, str::FromStr};
#[derive(serde::Serialize, serde::Deserialize)]
enum BinaryWire<'a> {
Code(u32),
Slug(::std::borrow::Cow<'a, str>),
}
impl serde::Serialize for SampleFormat {
fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
if ser.is_human_readable() {
match self {
SampleFormat::Unknown(v) => ser.serialize_u32(*v),
other => ser.serialize_str(other.as_str()),
}
} else {
match self {
SampleFormat::Unknown(v) => BinaryWire::Code(*v).serialize(ser),
other => BinaryWire::Slug(::std::borrow::Cow::Borrowed(other.as_str())).serialize(ser),
}
}
}
}
impl<'de> serde::Deserialize<'de> for SampleFormat {
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
if de.is_human_readable() {
struct V;
impl serde::de::Visitor<'_> for V {
type Value = SampleFormat;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a SampleFormat slug string or a u32 FFmpeg code")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
SampleFormat::from_str(v).map_err(serde::de::Error::custom)
}
fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
Ok(SampleFormat::from_u32(v))
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
u32::try_from(v)
.map(SampleFormat::from_u32)
.map_err(|_| serde::de::Error::custom("SampleFormat u32 code overflow"))
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
u32::try_from(v)
.map(SampleFormat::from_u32)
.map_err(|_| serde::de::Error::custom("SampleFormat u32 code out of range"))
}
}
de.deserialize_any(V)
} else {
let w = BinaryWire::deserialize(de)?;
Ok(match w {
BinaryWire::Code(v) => SampleFormat::from_u32(v),
BinaryWire::Slug(s) => SampleFormat::from_str(&s).unwrap(),
})
}
}
}
};
serde_via_code!(crate::color::Matrix);
serde_via_code!(crate::color::Primaries);
serde_via_code!(crate::color::Transfer);
serde_via_code!(crate::color::DynamicRange);
serde_via_code!(crate::color::ChromaLocation);
serde_via_code!(crate::color::DcpTargetGamut);
serde_via_code!(crate::pixel_format::PixelFormat);
serde_via_code!(crate::frame::Rotation);
serde_via_code!(crate::frame::FieldOrder);
serde_via_code!(crate::frame::StereoMode);
serde_via_code!(crate::disposition::TrackDisposition);
#[cfg(any(feature = "std", feature = "alloc"))]
serde_via_str!(crate::codec::VideoCodec);
#[cfg(any(feature = "std", feature = "alloc"))]
serde_via_str!(crate::codec::AudioCodec);
#[cfg(any(feature = "std", feature = "alloc"))]
serde_via_str!(crate::codec::SubtitleCodec);
#[cfg(any(feature = "std", feature = "alloc"))]
serde_via_str!(crate::container::Format);
#[cfg(any(feature = "std", feature = "alloc"))]
serde_via_str!(crate::subtitle::Format);
#[cfg(any(feature = "std", feature = "alloc"))]
serde_via_str!(crate::audio::ChannelLayout);
#[cfg(any(feature = "std", feature = "alloc"))]
serde_via_str!(crate::audio::ContainerFormat);
#[cfg(any(feature = "std", feature = "alloc"))]
serde_via_code_strict!(crate::subtitle::TrackOrigin);
#[cfg(any(feature = "std", feature = "alloc"))]
serde_via_code_strict!(crate::audio::BitRateMode);
#[cfg(all(test, feature = "std"))]
mod tests {
use crate::{
audio::{ChannelLayout, CoverArt, Fingerprint, Tags},
capture::GeoLocation,
codec::VideoCodec,
color::{self, Matrix},
disposition::TrackDisposition,
frame::{Dimensions, SampleAspectRatio},
lang::Language,
};
fn round_trip<T>(v: &T) -> T
where
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + core::fmt::Debug,
{
let json = serde_json::to_string(v).unwrap();
let back: T = serde_json::from_str(&json).unwrap();
assert_eq!(*v, back, "round-trip mismatch via {json}");
back
}
#[test]
fn open_enum_serializes_as_slug() {
assert_eq!(
serde_json::to_string(&VideoCodec::H264).unwrap(),
"\"h264\""
);
round_trip(&VideoCodec::H264);
let custom = VideoCodec::Other(smol_str::SmolStr::new("zzcodec"));
assert_eq!(serde_json::to_string(&custom).unwrap(), "\"zzcodec\"");
round_trip(&custom);
round_trip(&ChannelLayout::default());
}
#[test]
fn closed_enum_serializes_as_code() {
let json = serde_json::to_string(&Matrix::Bt709).unwrap();
assert_eq!(json, Matrix::Bt709.to_u32().to_string());
round_trip(&Matrix::Bt709);
let unknown: Matrix = serde_json::from_str("250").unwrap();
assert_eq!(unknown.to_u32(), 250);
}
#[test]
fn structs_round_trip() {
round_trip(&color::Info::default());
round_trip(&Dimensions::new(1920, 1080));
round_trip(&SampleAspectRatio::new(
40,
core::num::NonZeroU32::new(33).unwrap(),
));
round_trip(&Tags::new().with_title("Song").with_year(2026));
round_trip(&(TrackDisposition::DEFAULT | TrackDisposition::FORCED));
}
#[test]
fn language_round_trips_as_bcp47() {
let l = Language::from_bcp47("zh-Hant-TW").unwrap();
assert_eq!(serde_json::to_string(&l).unwrap(), "\"zh-Hant-TW\"");
round_trip(&l);
round_trip(&Language::default());
}
#[test]
fn validated_structs_check_on_deserialize() {
let g = GeoLocation::try_new(48.8584, 2.2945, Some(330.0)).unwrap();
round_trip(&g);
assert!(
serde_json::from_str::<GeoLocation>(r#"{"lat":999.0,"lon":0.0,"altitude":null}"#).is_err()
);
let fp = Fingerprint::try_new("chromaprint", &b"\x01\x02\x03"[..]).unwrap();
round_trip(&fp);
assert!(serde_json::from_str::<Fingerprint>(r#"{"algorithm":"","value":[1,2,3]}"#).is_err());
let art = CoverArt::try_new("image/png", &b"\x89PNG"[..]).unwrap();
round_trip(&art);
assert!(serde_json::from_str::<CoverArt>(r#"{"mime":"","data":[1]}"#).is_err());
}
#[test]
fn sample_format_preserves_unknown_u32() {
use crate::audio::SampleFormat;
assert_eq!(
serde_json::to_string(&SampleFormat::S16).unwrap(),
"\"s16\""
);
round_trip(&SampleFormat::S16);
let other = SampleFormat::Other(smol_str::SmolStr::new("custom"));
assert_eq!(serde_json::to_string(&other).unwrap(), "\"custom\"");
round_trip(&other);
for v in [12_345u32, 0xDEAD_BEEFu32, u32::MAX] {
let fmt = SampleFormat::Unknown(v);
assert_eq!(serde_json::to_string(&fmt).unwrap(), v.to_string());
let back: SampleFormat = serde_json::from_str(&v.to_string()).unwrap();
assert_eq!(back, fmt, "lost Unknown({v}) on round-trip");
}
let from_named_code: SampleFormat = serde_json::from_str("1").unwrap();
assert_eq!(from_named_code, SampleFormat::S16);
}
#[test]
fn closed_coded_enums_reject_unknown_codes() {
use crate::{audio::BitRateMode, subtitle::TrackOrigin};
for o in [
TrackOrigin::Embedded,
TrackOrigin::Sidecar,
TrackOrigin::External,
] {
round_trip(&o);
}
for m in [BitRateMode::Cbr, BitRateMode::Vbr, BitRateMode::Abr] {
round_trip(&m);
}
assert!(serde_json::from_str::<TrackOrigin>("999").is_err());
assert!(serde_json::from_str::<TrackOrigin>("3").is_err());
assert!(serde_json::from_str::<BitRateMode>("999").is_err());
assert!(serde_json::from_str::<BitRateMode>("3").is_err());
}
#[test]
fn sample_format_postcard_binary_roundtrip() {
use crate::audio::SampleFormat;
fn binary_round_trip(v: &SampleFormat) -> SampleFormat {
let bytes = postcard::to_allocvec(v).expect("postcard serialize");
postcard::from_bytes::<SampleFormat>(&bytes).expect("postcard deserialize")
}
assert_eq!(binary_round_trip(&SampleFormat::S16), SampleFormat::S16);
let other = SampleFormat::Other(smol_str::SmolStr::new("custom"));
assert_eq!(binary_round_trip(&other), other);
for v in [12_345u32, 0xDEAD_BEEFu32, u32::MAX] {
let fmt = SampleFormat::Unknown(v);
let back = binary_round_trip(&fmt);
assert_eq!(back, fmt, "lost Unknown({v}) on postcard round-trip");
}
}
#[test]
fn sparse_json_uses_serde_default_on_default_backed_structs() {
use crate::{
audio::{Loudness, Tags},
capture::Device,
};
let t: Tags = serde_json::from_str(r#"{"title":"hello"}"#).unwrap();
let expected = Tags::new().with_title(smol_str::SmolStr::new("hello"));
assert_eq!(t, expected);
let empty: Tags = serde_json::from_str("{}").unwrap();
assert_eq!(empty, Tags::default());
let d: Device = serde_json::from_str(r#"{"make":"Apple"}"#).unwrap();
let expected = Device::new().with_make(smol_str::SmolStr::new("Apple"));
assert_eq!(d, expected);
let l: Loudness = serde_json::from_str(r#"{"integrated_lufs":-23.0}"#).unwrap();
assert_eq!(l, Loudness::new(-23.0, 0.0, 0.0, 0.0));
}
#[test]
fn absent_option_fields_are_omitted_not_null() {
use crate::{audio::Tags, capture::GeoLocation, color::HdrStaticMetadata};
let j = serde_json::to_string(&Tags::default()).unwrap();
assert!(!j.contains("null"), "Tags emitted `null`: {j}");
assert!(
!j.contains("language"),
"Tags emitted absent `language`: {j}"
);
let j = serde_json::to_string(&HdrStaticMetadata::default()).unwrap();
assert_eq!(
j, "{}",
"empty HdrStaticMetadata should serialize to `{{}}`"
);
let g = GeoLocation::try_new(0.0, 0.0, None).unwrap();
let j = serde_json::to_string(&g).unwrap();
assert!(!j.contains("null"), "GeoLocation emitted `null`: {j}");
assert!(
!j.contains("altitude"),
"GeoLocation emitted absent `altitude`: {j}"
);
round_trip(&GeoLocation::try_new(0.0, 0.0, Some(12.5)).unwrap());
}
}