use serde::de::{DeserializeOwned, Error as _};
use serde::{Deserialize, Deserializer};
use serde_json::Value;
use crate::spec::{
Canvas, EasingChoice, FrameRateChoice, Inset, Loudness, MediaItem, Node, ThemeChoice,
TransitionChoice,
};
const MARK: char = '\u{0}';
pub(crate) struct Located {
pub path: String,
pub message: String,
}
pub(crate) fn join(outer: &str, inner: &str) -> String {
match (outer, inner) {
(".", inner) => inner.to_owned(),
(outer, ".") => outer.to_owned(),
(outer, inner) if inner.starts_with('[') => format!("{outer}{inner}"),
(outer, inner) => format!("{outer}.{inner}"),
}
}
pub(crate) fn from_value<T: DeserializeOwned>(value: Value) -> Result<T, Located> {
serde_path_to_error::deserialize(value).map_err(|error| {
let outer = error.path().to_string();
let message = error.into_inner().to_string();
match message
.strip_prefix(MARK)
.and_then(|rest| rest.split_once(MARK))
{
Some((inner, message)) => Located {
path: join(&outer, inner),
message: message.to_owned(),
},
None => Located {
path: outer,
message,
},
}
})
}
#[allow(clippy::needless_pass_by_value)]
fn carry<E: serde::de::Error>(located: Located) -> E {
E::custom(format!("{MARK}{}{MARK}{}", located.path, located.message))
}
fn kind_of(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "a list",
Value::Object(_) => "an object",
}
}
fn node(mut value: Value) -> Result<Node, Located> {
let here = |message: String| Located {
path: ".".to_owned(),
message,
};
let Value::Object(map) = &mut value else {
return Err(here(format!(
"a node is an object with a `type`, not {}",
kind_of(&value)
)));
};
let tag = match map.remove("type") {
Some(Value::String(tag)) => tag,
Some(other) => {
return Err(Located {
path: "type".to_owned(),
message: format!("a node's type is a string, not {}", kind_of(&other)),
});
}
None => return Err(here("a node has a `type`".to_owned())),
};
match tag.as_str() {
"sequence" => from_value(value).map(Node::Sequence),
"stack" => from_value(value).map(Node::Stack),
"grid" => from_value(value).map(Node::Grid),
"media" => from_value(value).map(Node::Media),
"text" => from_value(value).map(Node::Text),
"color" => from_value(value).map(Node::Color),
"captions" => from_value(value).map(Node::Captions),
"layout" => from_value(value).map(Node::Layout),
"audio" => from_value(value).map(Node::Audio),
"raw" => from_value(value).map(Node::Raw),
"slideshow" => from_value(value).map(Node::Slideshow),
"captioned_video" => from_value(value).map(Node::CaptionedVideo),
"title" => from_value(value).map(Node::Title),
"title_card" => from_value(value).map(Node::TitleCard),
"lower_third" => from_value(value).map(Node::LowerThird),
"end_card" => from_value(value).map(Node::EndCard),
"comparison_grid" => from_value(value).map(Node::ComparisonGrid),
"reframe" => from_value(value).map(Node::Reframe),
"logo" => from_value(value).map(Node::Logo),
"music_bed" => from_value(value).map(Node::MusicBed),
"counter" => from_value(value).map(Node::Counter),
other => Err(Located {
path: "type".to_owned(),
message: format!(
"unknown node type \"{other}\"; the types are {}",
NODE_TYPES.join(", ")
),
}),
}
}
pub(crate) const NODE_TYPES: [&str; 21] = [
"sequence",
"stack",
"grid",
"media",
"text",
"color",
"captions",
"layout",
"audio",
"raw",
"slideshow",
"captioned_video",
"title",
"title_card",
"lower_third",
"end_card",
"comparison_grid",
"reframe",
"logo",
"music_bed",
"counter",
];
impl<'de> Deserialize<'de> for Node {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
node(Value::deserialize(deserializer)?).map_err(carry)
}
}
fn by_type<T, E: serde::de::Error>(
value: Value,
string: impl FnOnce(String) -> T,
other: impl FnOnce(Value) -> Result<T, Located>,
) -> Result<T, E> {
match value {
Value::String(text) => Ok(string(text)),
value => other(value).map_err(carry),
}
}
fn object<T: DeserializeOwned>(value: Value, expected: &str) -> Result<T, Located> {
if value.is_object() {
from_value(value)
} else {
Err(Located {
path: ".".to_owned(),
message: format!("expected {expected}, not {}", kind_of(&value)),
})
}
}
impl<'de> Deserialize<'de> for MediaItem {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
by_type(Value::deserialize(deserializer)?, Self::Path, |value| {
node(value).map(|node| Self::Node(Box::new(node)))
})
}
}
impl<'de> Deserialize<'de> for Canvas {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
by_type(Value::deserialize(deserializer)?, Self::Preset, |value| {
object(value, "a preset name or a canvas object").map(Self::Custom)
})
}
}
impl<'de> Deserialize<'de> for ThemeChoice {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
by_type(Value::deserialize(deserializer)?, Self::Named, |value| {
object(value, "a theme name or a theme object")
.map(|theme| Self::Custom(Box::new(theme)))
})
}
}
impl<'de> Deserialize<'de> for TransitionChoice {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
by_type(Value::deserialize(deserializer)?, Self::Named, |value| {
object(value, "a transition name or a transition object").map(Self::Given)
})
}
}
impl<'de> Deserialize<'de> for EasingChoice {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
by_type(Value::deserialize(deserializer)?, Self::Named, |value| {
object(value, "an easing name or an easing object").map(Self::Curve)
})
}
}
impl<'de> Deserialize<'de> for Loudness {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
match Value::deserialize(deserializer)? {
Value::Number(number) => number
.as_f64()
.map(Self::Lufs)
.ok_or_else(|| D::Error::custom("the loudness is a number of LUFS")),
value => object(
value,
"a number of LUFS or {target_lufs, true_peak_ceiling_dbtp}",
)
.map(Self::Target)
.map_err(carry),
}
}
}
impl<'de> Deserialize<'de> for Inset {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = Value::deserialize(deserializer)?;
match &value {
#[allow(clippy::cast_possible_truncation)]
Value::Number(number) => number
.as_f64()
.map(|pixels| Self::Uniform(pixels as f32))
.ok_or_else(|| D::Error::custom("an inset is pixels")),
Value::Array(_) => from_value(value).map(Self::Sides).map_err(carry),
other => Err(D::Error::custom(format!(
"an inset is pixels, or [top, right, bottom, left] pixels, not {}",
kind_of(other)
))),
}
}
}
impl<'de> Deserialize<'de> for FrameRateChoice {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
match Value::deserialize(deserializer)? {
Value::Number(number) => number
.as_f64()
.map(Self::Number)
.ok_or_else(|| D::Error::custom("a frame rate is a number")),
value => object(value, "frames per second, or {num, den}")
.map(Self::Exact)
.map_err(carry),
}
}
}