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