Skip to main content

zencodec/
error.rs

1//! Error chain helpers for codec error inspection.
2//!
3//! Codec errors are typically nested: `BoxedError` → `At<MyCodecError>` →
4//! `LimitExceeded`. [`CodecErrorExt`] provides convenient methods to find
5//! common cause types. [`find_cause`] is the generic version for arbitrary
6//! error types.
7//!
8//! Works with `thiserror` `#[from]` variants, `whereat::At<E>` wrappers,
9//! and any error type that properly implements `source()`.
10
11use crate::{LimitExceeded, UnsupportedOperation};
12
13/// Extension trait for inspecting codec errors.
14///
15/// Blanket-implemented for all `core::error::Error + 'static` types.
16/// Walks the [`source()`](core::error::Error::source) chain to find
17/// common codec error causes without knowing the concrete error type.
18///
19/// Works through any wrapper that delegates `source()`:
20/// `thiserror` `#[from]` variants, `whereat::At<E>`, `Box<dyn Error>`, etc.
21///
22/// # Example
23///
24/// ```rust,ignore
25/// use zencodec::CodecErrorExt;
26///
27/// let result = dyn_decoder.decode();
28/// if let Err(ref e) = result {
29///     if let Some(limit) = e.limit_exceeded() {
30///         eprintln!("limit exceeded: {limit}");
31///     } else if let Some(op) = e.unsupported_operation() {
32///         eprintln!("not supported: {op}");
33///     }
34/// }
35/// ```
36pub trait CodecErrorExt {
37    /// Find an [`UnsupportedOperation`] in this error's cause chain.
38    fn unsupported_operation(&self) -> Option<&UnsupportedOperation>;
39
40    /// Find a [`LimitExceeded`] in this error's cause chain.
41    fn limit_exceeded(&self) -> Option<&LimitExceeded>;
42
43    /// Find a cause of arbitrary type `T` in this error's cause chain.
44    fn find_cause<T: core::error::Error + 'static>(&self) -> Option<&T>;
45}
46
47impl<E: core::error::Error + 'static> CodecErrorExt for E {
48    fn unsupported_operation(&self) -> Option<&UnsupportedOperation> {
49        find_cause::<UnsupportedOperation>(self)
50    }
51
52    fn limit_exceeded(&self) -> Option<&LimitExceeded> {
53        find_cause::<LimitExceeded>(self)
54    }
55
56    fn find_cause<T: core::error::Error + 'static>(&self) -> Option<&T> {
57        find_cause::<T>(self)
58    }
59}
60
61// Manual impl for trait objects — the blanket impl requires Sized.
62impl CodecErrorExt for dyn core::error::Error + Send + Sync + 'static {
63    fn unsupported_operation(&self) -> Option<&UnsupportedOperation> {
64        find_cause::<UnsupportedOperation>(self)
65    }
66
67    fn limit_exceeded(&self) -> Option<&LimitExceeded> {
68        find_cause::<LimitExceeded>(self)
69    }
70
71    fn find_cause<T: core::error::Error + 'static>(&self) -> Option<&T> {
72        find_cause::<T>(self)
73    }
74}
75
76impl CodecErrorExt for dyn core::error::Error + Send + 'static {
77    fn unsupported_operation(&self) -> Option<&UnsupportedOperation> {
78        find_cause::<UnsupportedOperation>(self)
79    }
80
81    fn limit_exceeded(&self) -> Option<&LimitExceeded> {
82        find_cause::<LimitExceeded>(self)
83    }
84
85    fn find_cause<T: core::error::Error + 'static>(&self) -> Option<&T> {
86        find_cause::<T>(self)
87    }
88}
89
90impl CodecErrorExt for dyn core::error::Error + 'static {
91    fn unsupported_operation(&self) -> Option<&UnsupportedOperation> {
92        find_cause::<UnsupportedOperation>(self)
93    }
94
95    fn limit_exceeded(&self) -> Option<&LimitExceeded> {
96        find_cause::<LimitExceeded>(self)
97    }
98
99    fn find_cause<T: core::error::Error + 'static>(&self) -> Option<&T> {
100        find_cause::<T>(self)
101    }
102}
103
104/// Walk an error's [`source()`](core::error::Error::source) chain to find
105/// a cause of type `T`.
106///
107/// Starts with the error itself, then follows `source()` links. Returns
108/// the first match.
109///
110/// Prefer [`CodecErrorExt`] methods for common types. Use this for
111/// codec-specific error types not covered by the extension trait.
112pub fn find_cause<'a, T: core::error::Error + 'static>(
113    mut err: &'a (dyn core::error::Error + 'static),
114) -> Option<&'a T> {
115    loop {
116        if let Some(t) = err.downcast_ref::<T>() {
117            return Some(t);
118        }
119        err = err.source()?;
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use alloc::boxed::Box;
127    use alloc::string::String;
128    use core::fmt;
129
130    // A simple codec error with source() chain via manual impl
131    #[derive(Debug)]
132    enum TestCodecError {
133        Limit(LimitExceeded),
134        Unsupported(UnsupportedOperation),
135        Other(String),
136    }
137
138    impl fmt::Display for TestCodecError {
139        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140            match self {
141                Self::Limit(e) => write!(f, "limit: {e}"),
142                Self::Unsupported(e) => write!(f, "unsupported: {e}"),
143                Self::Other(s) => write!(f, "other: {s}"),
144            }
145        }
146    }
147
148    impl core::error::Error for TestCodecError {
149        fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
150            match self {
151                Self::Limit(e) => Some(e),
152                Self::Unsupported(e) => Some(e),
153                Self::Other(_) => None,
154            }
155        }
156    }
157
158    #[test]
159    fn ext_limit_exceeded_direct() {
160        let err = LimitExceeded::Width {
161            actual: 5000,
162            max: 4096,
163        };
164        assert_eq!(err.limit_exceeded(), Some(&err));
165    }
166
167    #[test]
168    fn ext_limit_exceeded_through_source_chain() {
169        let inner = LimitExceeded::Pixels {
170            actual: 100_000_000,
171            max: 50_000_000,
172        };
173        let err = TestCodecError::Limit(inner.clone());
174        assert_eq!(err.limit_exceeded(), Some(&inner));
175    }
176
177    #[test]
178    fn ext_unsupported_through_source_chain() {
179        let err = TestCodecError::Unsupported(UnsupportedOperation::AnimationEncode);
180        assert_eq!(
181            err.unsupported_operation(),
182            Some(&UnsupportedOperation::AnimationEncode)
183        );
184    }
185
186    #[test]
187    fn ext_returns_none_when_absent() {
188        let err = TestCodecError::Other("something else".into());
189        assert!(err.limit_exceeded().is_none());
190        assert!(err.unsupported_operation().is_none());
191    }
192
193    #[test]
194    fn ext_through_boxed_error() {
195        let inner = LimitExceeded::Memory {
196            actual: 1_000_000_000,
197            max: 512_000_000,
198        };
199        let err = TestCodecError::Limit(inner.clone());
200        let boxed: Box<dyn core::error::Error + Send + Sync> = Box::new(err);
201        assert_eq!(boxed.limit_exceeded(), Some(&inner));
202    }
203
204    #[test]
205    fn ext_find_cause_generic() {
206        let err = TestCodecError::Unsupported(UnsupportedOperation::DecodeInto);
207        let found: Option<&UnsupportedOperation> = err.find_cause();
208        assert_eq!(found, Some(&UnsupportedOperation::DecodeInto));
209    }
210
211    // find_cause free function still works
212    #[test]
213    fn find_cause_free_fn() {
214        let err = LimitExceeded::Width {
215            actual: 5000,
216            max: 4096,
217        };
218        let found = find_cause::<LimitExceeded>(&err);
219        assert_eq!(found, Some(&err));
220    }
221}