1use serde::de::{DeserializeOwned, Error as _};
9use serde::{Deserialize, Deserializer};
10use serde_json::Value;
11
12use crate::spec::{
13 Canvas, FrameRateChoice, Inset, Loudness, MediaItem, Node, ThemeChoice, TransitionChoice,
14};
15
16const MARK: char = '\u{0}';
18
19pub(crate) struct Located {
21 pub path: String,
23 pub message: String,
24}
25
26pub(crate) fn join(outer: &str, inner: &str) -> String {
28 match (outer, inner) {
29 (".", inner) => inner.to_owned(),
30 (outer, ".") => outer.to_owned(),
31 (outer, inner) if inner.starts_with('[') => format!("{outer}{inner}"),
32 (outer, inner) => format!("{outer}.{inner}"),
33 }
34}
35
36pub(crate) fn from_value<T: DeserializeOwned>(value: Value) -> Result<T, Located> {
38 serde_path_to_error::deserialize(value).map_err(|error| {
39 let outer = error.path().to_string();
40 let message = error.into_inner().to_string();
41 match message
42 .strip_prefix(MARK)
43 .and_then(|rest| rest.split_once(MARK))
44 {
45 Some((inner, message)) => Located {
46 path: join(&outer, inner),
47 message: message.to_owned(),
48 },
49 None => Located {
50 path: outer,
51 message,
52 },
53 }
54 })
55}
56
57#[allow(clippy::needless_pass_by_value)]
59fn carry<E: serde::de::Error>(located: Located) -> E {
60 E::custom(format!("{MARK}{}{MARK}{}", located.path, located.message))
61}
62
63fn kind_of(value: &Value) -> &'static str {
64 match value {
65 Value::Null => "null",
66 Value::Bool(_) => "a boolean",
67 Value::Number(_) => "a number",
68 Value::String(_) => "a string",
69 Value::Array(_) => "a list",
70 Value::Object(_) => "an object",
71 }
72}
73
74fn node(mut value: Value) -> Result<Node, Located> {
75 let here = |message: String| Located {
76 path: ".".to_owned(),
77 message,
78 };
79 let Value::Object(map) = &mut value else {
80 return Err(here(format!(
81 "a node is an object with a `type`, not {}",
82 kind_of(&value)
83 )));
84 };
85 let tag = match map.remove("type") {
86 Some(Value::String(tag)) => tag,
87 Some(other) => {
88 return Err(Located {
89 path: "type".to_owned(),
90 message: format!("a node's type is a string, not {}", kind_of(&other)),
91 });
92 }
93 None => return Err(here("a node has a `type`".to_owned())),
94 };
95 match tag.as_str() {
96 "sequence" => from_value(value).map(Node::Sequence),
97 "stack" => from_value(value).map(Node::Stack),
98 "grid" => from_value(value).map(Node::Grid),
99 "media" => from_value(value).map(Node::Media),
100 "text" => from_value(value).map(Node::Text),
101 "color" => from_value(value).map(Node::Color),
102 "captions" => from_value(value).map(Node::Captions),
103 "layout" => from_value(value).map(Node::Layout),
104 "audio" => from_value(value).map(Node::Audio),
105 "raw" => from_value(value).map(Node::Raw),
106 "slideshow" => from_value(value).map(Node::Slideshow),
107 "captioned_video" => from_value(value).map(Node::CaptionedVideo),
108 "title" => from_value(value).map(Node::Title),
109 "title_card" => from_value(value).map(Node::TitleCard),
110 "lower_third" => from_value(value).map(Node::LowerThird),
111 "end_card" => from_value(value).map(Node::EndCard),
112 "comparison_grid" => from_value(value).map(Node::ComparisonGrid),
113 "reframe" => from_value(value).map(Node::Reframe),
114 "logo" => from_value(value).map(Node::Logo),
115 "music_bed" => from_value(value).map(Node::MusicBed),
116 other => Err(Located {
117 path: "type".to_owned(),
118 message: format!(
119 "unknown node type \"{other}\"; the types are {}",
120 NODE_TYPES.join(", ")
121 ),
122 }),
123 }
124}
125
126pub(crate) const NODE_TYPES: [&str; 20] = [
128 "sequence",
129 "stack",
130 "grid",
131 "media",
132 "text",
133 "color",
134 "captions",
135 "layout",
136 "audio",
137 "raw",
138 "slideshow",
139 "captioned_video",
140 "title",
141 "title_card",
142 "lower_third",
143 "end_card",
144 "comparison_grid",
145 "reframe",
146 "logo",
147 "music_bed",
148];
149
150impl<'de> Deserialize<'de> for Node {
151 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
152 node(Value::deserialize(deserializer)?).map_err(carry)
153 }
154}
155
156fn by_type<T, E: serde::de::Error>(
158 value: Value,
159 string: impl FnOnce(String) -> T,
160 other: impl FnOnce(Value) -> Result<T, Located>,
161) -> Result<T, E> {
162 match value {
163 Value::String(text) => Ok(string(text)),
164 value => other(value).map_err(carry),
165 }
166}
167
168fn object<T: DeserializeOwned>(value: Value, expected: &str) -> Result<T, Located> {
169 if value.is_object() {
170 from_value(value)
171 } else {
172 Err(Located {
173 path: ".".to_owned(),
174 message: format!("expected {expected}, not {}", kind_of(&value)),
175 })
176 }
177}
178
179impl<'de> Deserialize<'de> for MediaItem {
180 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
181 by_type(Value::deserialize(deserializer)?, Self::Path, |value| {
182 node(value).map(|node| Self::Node(Box::new(node)))
183 })
184 }
185}
186
187impl<'de> Deserialize<'de> for Canvas {
188 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
189 by_type(Value::deserialize(deserializer)?, Self::Preset, |value| {
190 object(value, "a preset name or a canvas object").map(Self::Custom)
191 })
192 }
193}
194
195impl<'de> Deserialize<'de> for ThemeChoice {
196 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
197 by_type(Value::deserialize(deserializer)?, Self::Named, |value| {
198 object(value, "a theme name or a theme object")
199 .map(|theme| Self::Custom(Box::new(theme)))
200 })
201 }
202}
203
204impl<'de> Deserialize<'de> for TransitionChoice {
205 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
206 by_type(Value::deserialize(deserializer)?, Self::Named, |value| {
207 object(value, "a transition name or a transition object").map(Self::Given)
208 })
209 }
210}
211
212impl<'de> Deserialize<'de> for Loudness {
213 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
214 match Value::deserialize(deserializer)? {
215 Value::Number(number) => number
216 .as_f64()
217 .map(Self::Lufs)
218 .ok_or_else(|| D::Error::custom("the loudness is a number of LUFS")),
219 value => object(
220 value,
221 "a number of LUFS or {target_lufs, true_peak_ceiling_dbtp}",
222 )
223 .map(Self::Target)
224 .map_err(carry),
225 }
226 }
227}
228
229impl<'de> Deserialize<'de> for Inset {
230 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
231 let value = Value::deserialize(deserializer)?;
232 match &value {
233 #[allow(clippy::cast_possible_truncation)]
234 Value::Number(number) => number
235 .as_f64()
236 .map(|pixels| Self::Uniform(pixels as f32))
237 .ok_or_else(|| D::Error::custom("an inset is pixels")),
238 Value::Array(_) => from_value(value).map(Self::Sides).map_err(carry),
239 other => Err(D::Error::custom(format!(
240 "an inset is pixels, or [top, right, bottom, left] pixels, not {}",
241 kind_of(other)
242 ))),
243 }
244 }
245}
246
247impl<'de> Deserialize<'de> for FrameRateChoice {
248 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
249 match Value::deserialize(deserializer)? {
250 Value::Number(number) => number
251 .as_f64()
252 .map(Self::Number)
253 .ok_or_else(|| D::Error::custom("a frame rate is a number")),
254 value => object(value, "frames per second, or {num, den}")
255 .map(Self::Exact)
256 .map_err(carry),
257 }
258 }
259}