Skip to main content

alpha2_compose/
de.rs

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