1use std::{error::Error as StdError, fmt, io};
4
5use crate::ValidationError;
6
7pub type BoxedError = Box<dyn StdError + Send + Sync + 'static>;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum ErrorKind {
14 Validation,
16 Io,
18 Asset,
20 Svg,
22 Formula,
24 Parameters,
26 Timeline,
28 GpuUnavailable,
30 Gpu,
32 DeviceLost,
34 ScenePanic,
36 Encoder,
38 Cancelled,
40 Other,
42}
43
44#[derive(Debug)]
58#[non_exhaustive]
59pub enum Error {
60 BuildErrors(Vec<std::sync::Arc<Error>>),
62 Validation(ValidationError),
64 Io(io::Error),
66 Asset {
68 kind: &'static str,
70 source_name: String,
72 cause: BoxedError,
74 },
75 Svg(BoxedError),
77 Formula(String),
79 FormulaDiagnostics(BoxedError),
81 Parameters(BoxedError),
83 Timeline {
85 timeline: String,
87 message: String,
89 },
90 GpuUnavailable(BoxedError),
92 Gpu(BoxedError),
94 DeviceLost(BoxedError),
96 ScenePanic {
98 timeline: String,
100 frame: u64,
102 message: String,
104 },
105 Encoder(BoxedError),
107 Cancelled,
109 AtFrame {
111 frame: u64,
113 cause: Box<Error>,
115 },
116 Other(BoxedError),
118}
119
120impl Error {
121 pub fn build_errors(&self) -> &[std::sync::Arc<Error>] {
123 match self {
124 Self::BuildErrors(errors) => errors,
125 Self::AtFrame { cause, .. } => cause.build_errors(),
126 _ => &[],
127 }
128 }
129
130 pub fn asset(
132 kind: &'static str,
133 source_name: impl Into<String>,
134 cause: impl StdError + Send + Sync + 'static,
135 ) -> Self {
136 Self::Asset {
137 kind,
138 source_name: source_name.into(),
139 cause: Box::new(cause),
140 }
141 }
142
143 pub fn kind(&self) -> ErrorKind {
145 match self {
146 Self::BuildErrors(errors) => errors
147 .first()
148 .map_or(ErrorKind::Validation, |error| error.kind()),
149 Self::Validation(_) => ErrorKind::Validation,
150 Self::Io(_) => ErrorKind::Io,
151 Self::Asset { .. } => ErrorKind::Asset,
152 Self::Svg(_) => ErrorKind::Svg,
153 Self::Formula(_) | Self::FormulaDiagnostics(_) => ErrorKind::Formula,
154 Self::Parameters(_) => ErrorKind::Parameters,
155 Self::Timeline { .. } => ErrorKind::Timeline,
156 Self::GpuUnavailable(_) => ErrorKind::GpuUnavailable,
157 Self::Gpu(_) => ErrorKind::Gpu,
158 Self::DeviceLost(_) => ErrorKind::DeviceLost,
159 Self::ScenePanic { .. } => ErrorKind::ScenePanic,
160 Self::Encoder(_) => ErrorKind::Encoder,
161 Self::Cancelled => ErrorKind::Cancelled,
162 Self::AtFrame { cause, .. } => cause.kind(),
163 Self::Other(_) => ErrorKind::Other,
164 }
165 }
166
167 pub fn frame(&self) -> Option<u64> {
169 match self {
170 Self::Validation(error) => error.frame.map(u64::from),
171 Self::ScenePanic { frame, .. } | Self::AtFrame { frame, .. } => Some(*frame),
172 _ => None,
173 }
174 }
175
176 pub fn at_frame(mut self, frame: u64) -> Self {
179 if self.frame().is_some() {
180 return self;
181 }
182 if let Self::Validation(error) = &mut self
183 && let Ok(frame) = u32::try_from(frame)
184 {
185 error.frame = Some(frame);
186 return self;
187 }
188 Self::AtFrame {
189 frame,
190 cause: Box::new(self),
191 }
192 }
193
194 pub fn downcast_ref<T: StdError + 'static>(&self) -> Option<&T> {
197 let mut current: Option<&(dyn StdError + 'static)> = Some(self);
198 while let Some(error) = current {
199 if let Some(error) = error.downcast_ref::<T>() {
200 return Some(error);
201 }
202 current = error.source();
203 }
204 None
205 }
206
207 pub fn scene_panic(
209 timeline: impl Into<String>,
210 frame: u64,
211 payload: Box<dyn std::any::Any + Send>,
212 ) -> Self {
213 let message = payload
214 .downcast_ref::<String>()
215 .cloned()
216 .or_else(|| {
217 payload
218 .downcast_ref::<&str>()
219 .map(|message| (*message).to_owned())
220 })
221 .unwrap_or_else(|| "non-string panic payload".to_owned());
222 Self::ScenePanic {
223 timeline: timeline.into(),
224 frame,
225 message,
226 }
227 }
228}
229
230impl fmt::Display for Error {
231 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
232 match self {
233 Self::BuildErrors(errors) => {
234 write!(formatter, "{} authoring error(s)", errors.len())?;
235 for error in errors {
236 write!(formatter, "\n {error}")?;
237 }
238 Ok(())
239 }
240 Self::Validation(error) => error.fmt(formatter),
241 Self::Io(error) => error.fmt(formatter),
242 Self::Asset {
243 kind,
244 source_name,
245 cause,
246 } => write!(
247 formatter,
248 "{kind} asset `{source_name}` failed to load: {cause}"
249 ),
250 Self::Svg(cause) => write!(formatter, "SVG parse error: {cause}"),
251 Self::Formula(message) => write!(formatter, "formula compile error: {message}"),
252 Self::FormulaDiagnostics(cause) => write!(formatter, "formula compile error: {cause}"),
253 Self::Parameters(cause) => write!(formatter, "invalid parameters: {cause}"),
254 Self::Timeline { timeline, message } => {
255 write!(formatter, "timeline `{timeline}`: {message}")
256 }
257 Self::GpuUnavailable(cause) => write!(formatter, "GPU unavailable: {cause}"),
258 Self::Gpu(cause) => write!(formatter, "GPU error: {cause}"),
259 Self::DeviceLost(cause) => write!(formatter, "GPU device lost: {cause}"),
260 Self::ScenePanic {
261 timeline,
262 frame,
263 message,
264 } => write!(
265 formatter,
266 "scene panicked at frame {frame} of `{timeline}`: {message}"
267 ),
268 Self::Encoder(cause) => write!(formatter, "encoder error: {cause}"),
269 Self::Cancelled => formatter.write_str("export cancelled"),
270 Self::AtFrame { frame, cause } => write!(formatter, "frame {frame}: {cause}"),
271 Self::Other(cause) => cause.fmt(formatter),
272 }
273 }
274}
275
276impl StdError for Error {
277 fn source(&self) -> Option<&(dyn StdError + 'static)> {
278 match self {
279 Self::Validation(error) => Some(error),
280 Self::Io(error) => Some(error),
281 Self::Asset { cause, .. }
282 | Self::Svg(cause)
283 | Self::FormulaDiagnostics(cause)
284 | Self::Parameters(cause)
285 | Self::GpuUnavailable(cause)
286 | Self::Gpu(cause)
287 | Self::DeviceLost(cause)
288 | Self::Encoder(cause)
289 | Self::Other(cause) => Some(cause.as_ref()),
290 Self::AtFrame { cause, .. } => Some(cause.as_ref()),
291 _ => None,
292 }
293 }
294}
295
296impl From<ValidationError> for Error {
297 fn from(error: ValidationError) -> Self {
298 if error.field == "formula" {
299 Self::FormulaDiagnostics(Box::new(error))
300 } else {
301 Self::Validation(error)
302 }
303 }
304}
305
306impl From<io::Error> for Error {
307 fn from(error: io::Error) -> Self {
308 Self::Io(error)
309 }
310}
311
312impl From<BoxedError> for Error {
313 fn from(error: BoxedError) -> Self {
314 Self::Other(error)
315 }
316}
317
318impl From<std::collections::TryReserveError> for Error {
319 fn from(error: std::collections::TryReserveError) -> Self {
320 Self::Io(io::Error::new(io::ErrorKind::OutOfMemory, error))
321 }
322}
323
324impl From<std::env::VarError> for Error {
325 fn from(error: std::env::VarError) -> Self {
326 Self::Parameters(Box::new(error))
327 }
328}
329
330impl From<std::str::Utf8Error> for Error {
331 fn from(error: std::str::Utf8Error) -> Self {
332 Self::Other(Box::new(error))
333 }
334}
335
336impl From<std::sync::mpsc::RecvError> for Error {
337 fn from(error: std::sync::mpsc::RecvError) -> Self {
338 Self::Gpu(Box::new(error))
339 }
340}
341
342impl From<std::array::TryFromSliceError> for Error {
343 fn from(error: std::array::TryFromSliceError) -> Self {
344 Self::Gpu(Box::new(error))
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn classification_frames_and_sources_survive_annotation() {
354 fn send_sync<T: Send + Sync>() {}
355 send_sync::<Error>();
356 let original = io::Error::new(io::ErrorKind::PermissionDenied, "destination denied");
357 let error = Error::Encoder(Box::new(original)).at_frame(7).at_frame(9);
358 assert_eq!(error.kind(), ErrorKind::Encoder);
359 assert_eq!(error.frame(), Some(7));
360 assert_eq!(
361 error.downcast_ref::<io::Error>().unwrap().kind(),
362 io::ErrorKind::PermissionDenied
363 );
364 assert!(
365 error
366 .to_string()
367 .contains("frame 7: encoder error: destination denied")
368 );
369 let original = ValidationError::invalid("nodes[2]", "opacity", "invalid opacity");
370 let error = Error::from(original).at_frame(11);
371 let diagnostic = error.downcast_ref::<ValidationError>().unwrap();
372 assert_eq!(diagnostic.frame, Some(11));
373 assert_eq!(diagnostic.path, "nodes[2]");
374 assert_eq!(error.frame(), Some(11));
375 assert_eq!(error.kind(), ErrorKind::Validation);
376 }
377
378 #[test]
379 fn panic_payloads_keep_scene_and_frame() {
380 for payload in [
381 Box::new("bad scene") as Box<dyn std::any::Any + Send>,
382 Box::new("bad scene".to_owned()),
383 ] {
384 let error = Error::scene_panic("intro", 23, payload);
385 assert_eq!(error.kind(), ErrorKind::ScenePanic);
386 assert_eq!(error.frame(), Some(23));
387 assert!(error.to_string().contains("intro"));
388 assert!(error.to_string().contains("bad scene"));
389 }
390 assert!(
391 Error::scene_panic("intro", 1, Box::new(42_u32))
392 .to_string()
393 .contains("non-string")
394 );
395 assert_eq!(Error::Cancelled.kind(), ErrorKind::Cancelled);
396 }
397}