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