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, FrameRateChoice, Inset, Loudness, MediaItem, Node, ThemeChoice, TransitionChoice,
14};
15
16/// Marks a message that carries the path below the value that failed.
17const MARK: char = '\u{0}';
18
19/// A problem and where it lies, below some value.
20pub(crate) struct Located {
21    /// `serde_path_to_error`'s form: `children[2].text`, or `.` for the value itself.
22    pub path: String,
23    pub message: String,
24}
25
26/// `inner` (a path below `outer`) joined to `outer`.
27pub(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
36/// Parses `value` as `T`, the path through any enum included.
37pub(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/// A serde error carrying `located`'s path up.
58#[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
126/// Every node type, as specs write them.
127pub(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
156/// Parses `value` by its JSON type: a string with `string`, anything else with `other`.
157fn 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}