Skip to main content

alpha2_compose/
de.rs

1//! Spec parsing that reports where in the spec an error lies.
2//!
3//! `serde_path_to_error` does not follow paths into the buffered content of tagged or
4//! untagged enums, which is where a spec's nodes are. Those enums parse by hand here: each
5//! picks its variant by tag or JSON type, parses it with its own path, and passes that path
6//! up in the error message for the level above to join.
7
8use 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
17/// Marks a message that carries the path below the value that failed.
18const MARK: char = '\u{0}';
19
20/// A problem and where it lies, below some value.
21pub(crate) struct Located {
22    /// `serde_path_to_error`'s form: `children[2].text`, or `.` for the value itself.
23    pub path: String,
24    pub message: String,
25}
26
27/// `inner` (a path below `outer`) joined to `outer`.
28pub(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
37/// Parses `value` as `T`, the path through any enum included.
38pub(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/// A serde error carrying `located`'s path up.
59#[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
128/// Every node type, as specs write them.
129pub(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
159/// Parses `value` by its JSON type: a string with `string`, anything else with `other`.
160fn 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}