1use std::{error::Error as StdError, fmt, io};
4
5use crate::{ErrorReport, 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 Validation(ValidationError),
62 Io(io::Error),
64 Asset {
66 kind: &'static str,
68 source_name: String,
70 cause: BoxedError,
72 },
73 Svg(BoxedError),
75 Formula {
77 message: String,
79 source: Option<BoxedError>,
81 },
82 Parameters(BoxedError),
84 Timeline {
86 timeline: String,
88 message: String,
90 },
91 GpuUnavailable(BoxedError),
93 Gpu(BoxedError),
95 DeviceLost(BoxedError),
97 ScenePanic {
99 timeline: String,
101 frame: u64,
103 message: String,
105 },
106 Encoder(BoxedError),
108 Cancelled,
110 AtFrame {
112 frame: u64,
114 cause: Box<Error>,
116 },
117 Other(BoxedError),
119}
120
121impl Error {
122 pub fn report(&self) -> Option<&ErrorReport> {
124 match self {
125 Self::Other(cause) => cause.downcast_ref::<ErrorReport>(),
126 Self::AtFrame { cause, .. } => cause.report(),
127 _ => None,
128 }
129 }
130
131 pub fn asset(
133 kind: &'static str,
134 source_name: impl Into<String>,
135 cause: impl StdError + Send + Sync + 'static,
136 ) -> Self {
137 Self::Asset {
138 kind,
139 source_name: source_name.into(),
140 cause: Box::new(cause),
141 }
142 }
143
144 pub fn formula(message: impl Into<String>) -> Self {
146 Self::Formula {
147 message: message.into(),
148 source: None,
149 }
150 }
151
152 pub fn formula_with_source(
154 message: impl Into<String>,
155 source: impl StdError + Send + Sync + 'static,
156 ) -> Self {
157 Self::Formula {
158 message: message.into(),
159 source: Some(Box::new(source)),
160 }
161 }
162
163 pub fn kind(&self) -> ErrorKind {
165 match self {
166 Self::Validation(_) => ErrorKind::Validation,
167 Self::Io(_) => ErrorKind::Io,
168 Self::Asset { .. } => ErrorKind::Asset,
169 Self::Svg(_) => ErrorKind::Svg,
170 Self::Formula { .. } => ErrorKind::Formula,
171 Self::Parameters(_) => ErrorKind::Parameters,
172 Self::Timeline { .. } => ErrorKind::Timeline,
173 Self::GpuUnavailable(_) => ErrorKind::GpuUnavailable,
174 Self::Gpu(_) => ErrorKind::Gpu,
175 Self::DeviceLost(_) => ErrorKind::DeviceLost,
176 Self::ScenePanic { .. } => ErrorKind::ScenePanic,
177 Self::Encoder(_) => ErrorKind::Encoder,
178 Self::Cancelled => ErrorKind::Cancelled,
179 Self::AtFrame { cause, .. } => cause.kind(),
180 Self::Other(cause) => {
181 cause
182 .downcast_ref::<ErrorReport>()
183 .map_or(ErrorKind::Other, |report| {
184 report
185 .first()
186 .map_or(ErrorKind::Validation, |error| error.kind())
187 })
188 }
189 }
190 }
191
192 pub fn frame(&self) -> Option<u64> {
194 match self {
195 Self::Validation(error) => error.frame.map(u64::from),
196 Self::ScenePanic { frame, .. } | Self::AtFrame { frame, .. } => Some(*frame),
197 _ => None,
198 }
199 }
200
201 pub fn at_frame(mut self, frame: u64) -> Self {
204 if self.frame().is_some() {
205 return self;
206 }
207 if let Self::Validation(error) = &mut self
208 && let Ok(frame) = u32::try_from(frame)
209 {
210 error.frame = Some(frame);
211 return self;
212 }
213 Self::AtFrame {
214 frame,
215 cause: Box::new(self),
216 }
217 }
218
219 pub fn downcast_ref<T: StdError + 'static>(&self) -> Option<&T> {
222 let mut current: Option<&(dyn StdError + 'static)> = Some(self);
223 while let Some(error) = current {
224 if let Some(error) = error.downcast_ref::<T>() {
225 return Some(error);
226 }
227 current = error.source();
228 }
229 None
230 }
231
232 pub fn scene_panic(
234 timeline: impl Into<String>,
235 frame: u64,
236 payload: Box<dyn std::any::Any + Send>,
237 ) -> Self {
238 let message = payload
239 .downcast_ref::<String>()
240 .cloned()
241 .or_else(|| {
242 payload
243 .downcast_ref::<&str>()
244 .map(|message| (*message).to_owned())
245 })
246 .unwrap_or_else(|| "non-string panic payload".to_owned());
247 Self::ScenePanic {
248 timeline: timeline.into(),
249 frame,
250 message,
251 }
252 }
253}
254
255impl fmt::Display for Error {
256 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
257 match self {
258 Self::Validation(error) => error.fmt(formatter),
259 Self::Io(error) => error.fmt(formatter),
260 Self::Asset {
261 kind,
262 source_name,
263 cause,
264 } => write!(
265 formatter,
266 "{kind} asset `{source_name}` failed to load: {cause}"
267 ),
268 Self::Svg(cause) => write!(formatter, "SVG parse error: {cause}"),
269 Self::Formula { message, .. } => write!(formatter, "formula compile error: {message}"),
270 Self::Parameters(cause) => write!(formatter, "invalid parameters: {cause}"),
271 Self::Timeline { timeline, message } => {
272 write!(formatter, "timeline `{timeline}`: {message}")
273 }
274 Self::GpuUnavailable(cause) => write!(formatter, "GPU unavailable: {cause}"),
275 Self::Gpu(cause) => write!(formatter, "GPU error: {cause}"),
276 Self::DeviceLost(cause) => write!(formatter, "GPU device lost: {cause}"),
277 Self::ScenePanic {
278 timeline,
279 frame,
280 message,
281 } => write!(
282 formatter,
283 "scene panicked at frame {frame} of `{timeline}`: {message}"
284 ),
285 Self::Encoder(cause) => write!(formatter, "encoder error: {cause}"),
286 Self::Cancelled => formatter.write_str("export cancelled"),
287 Self::AtFrame { frame, cause } => write!(formatter, "frame {frame}: {cause}"),
288 Self::Other(cause) => cause.fmt(formatter),
289 }
290 }
291}
292
293impl StdError for Error {
294 fn source(&self) -> Option<&(dyn StdError + 'static)> {
295 match self {
296 Self::Validation(error) => Some(error),
297 Self::Io(error) => Some(error),
298 Self::Asset { cause, .. }
299 | Self::Svg(cause)
300 | Self::Parameters(cause)
301 | Self::GpuUnavailable(cause)
302 | Self::Gpu(cause)
303 | Self::DeviceLost(cause)
304 | Self::Encoder(cause)
305 | Self::Other(cause) => Some(cause.as_ref()),
306 Self::Formula { source, .. } => source
307 .as_deref()
308 .map(|cause| cause as &(dyn StdError + 'static)),
309 Self::AtFrame { cause, .. } => Some(cause.as_ref()),
310 _ => None,
311 }
312 }
313}
314
315impl From<ValidationError> for Error {
316 fn from(error: ValidationError) -> Self {
317 if error.field == "formula" {
318 Self::Formula {
319 message: error.to_string(),
320 source: Some(Box::new(error)),
321 }
322 } else {
323 Self::Validation(error)
324 }
325 }
326}
327
328impl From<io::Error> for Error {
329 fn from(error: io::Error) -> Self {
330 Self::Io(error)
331 }
332}
333
334impl From<BoxedError> for Error {
335 fn from(error: BoxedError) -> Self {
336 Self::Other(error)
337 }
338}
339
340impl From<std::collections::TryReserveError> for Error {
341 fn from(error: std::collections::TryReserveError) -> Self {
342 Self::Io(io::Error::new(io::ErrorKind::OutOfMemory, error))
343 }
344}
345
346impl From<std::env::VarError> for Error {
347 fn from(error: std::env::VarError) -> Self {
348 Self::Parameters(Box::new(error))
349 }
350}
351
352impl From<std::str::Utf8Error> for Error {
353 fn from(error: std::str::Utf8Error) -> Self {
354 Self::Other(Box::new(error))
355 }
356}
357
358impl From<std::sync::mpsc::RecvError> for Error {
359 fn from(error: std::sync::mpsc::RecvError) -> Self {
360 Self::Gpu(Box::new(error))
361 }
362}
363
364impl From<std::array::TryFromSliceError> for Error {
365 fn from(error: std::array::TryFromSliceError) -> Self {
366 Self::Gpu(Box::new(error))
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn classification_frames_and_sources_survive_annotation() {
376 fn send_sync<T: Send + Sync>() {}
377 send_sync::<Error>();
378 let original = io::Error::new(io::ErrorKind::PermissionDenied, "destination denied");
379 let error = Error::Encoder(Box::new(original)).at_frame(7).at_frame(9);
380 assert_eq!(error.kind(), ErrorKind::Encoder);
381 assert_eq!(error.frame(), Some(7));
382 assert_eq!(
383 error.downcast_ref::<io::Error>().unwrap().kind(),
384 io::ErrorKind::PermissionDenied
385 );
386 assert!(
387 error
388 .to_string()
389 .contains("frame 7: encoder error: destination denied")
390 );
391 let original = ValidationError::invalid("nodes[2]", "opacity", "invalid opacity");
392 let error = Error::from(original).at_frame(11);
393 let diagnostic = error.downcast_ref::<ValidationError>().unwrap();
394 assert_eq!(diagnostic.frame, Some(11));
395 assert_eq!(diagnostic.path, "nodes[2]");
396 assert_eq!(error.frame(), Some(11));
397 assert_eq!(error.kind(), ErrorKind::Validation);
398 }
399
400 #[test]
401 fn panic_payloads_keep_scene_and_frame() {
402 for payload in [
403 Box::new("bad scene") as Box<dyn std::any::Any + Send>,
404 Box::new("bad scene".to_owned()),
405 ] {
406 let error = Error::scene_panic("intro", 23, payload);
407 assert_eq!(error.kind(), ErrorKind::ScenePanic);
408 assert_eq!(error.frame(), Some(23));
409 assert!(error.to_string().contains("intro"));
410 assert!(error.to_string().contains("bad scene"));
411 }
412 assert!(
413 Error::scene_panic("intro", 1, Box::new(42_u32))
414 .to_string()
415 .contains("non-string")
416 );
417 assert_eq!(Error::Cancelled.kind(), ErrorKind::Cancelled);
418 }
419}