Skip to main content

miden_protobuf/
error.rs

1use alloc::boxed::Box;
2use alloc::format;
3use alloc::string::String;
4use alloc::vec::Vec;
5use core::any::type_name;
6use core::error::Error;
7use core::fmt;
8
9/// Error produced while converting a Protobuf message into a domain object.
10#[derive(Debug)]
11pub struct ConversionError {
12    path: Vec<String>,
13    source: Box<dyn core::error::Error + Send + Sync>,
14}
15
16impl ConversionError {
17    pub fn new(source: impl Error + Send + Sync + 'static) -> Self {
18        let source: Box<dyn Error + Send + Sync> = Box::new(source);
19        match source.downcast::<Self>() {
20            Ok(error) => *error,
21            Err(source) => Self { path: Vec::new(), source },
22        }
23    }
24
25    #[must_use]
26    pub fn context(mut self, field: impl Into<String>) -> Self {
27        self.path.push(field.into());
28        self
29    }
30
31    pub fn missing_field<T: prost::Message>(field_name: &'static str) -> Self {
32        Self::message(format!("field {}::{field_name} is missing", type_name::<T>()))
33    }
34
35    pub fn deserialization(
36        entity: &'static str,
37        source: impl Error + Send + Sync + 'static,
38    ) -> Self {
39        let message = format!("failed to deserialize {entity}: {source}");
40        Self::with_source(message, source)
41    }
42
43    pub fn message(message: impl Into<String>) -> Self {
44        Self {
45            path: Vec::new(),
46            source: Box::new(StringError(message.into())),
47        }
48    }
49
50    pub fn with_source(
51        message: impl Into<String>,
52        source: impl Error + Send + Sync + 'static,
53    ) -> Self {
54        Self::new(ContextualError {
55            message: message.into(),
56            source: Box::new(source),
57        })
58    }
59}
60
61impl fmt::Display for ConversionError {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        for (index, segment) in self.path.iter().rev().enumerate() {
64            if index > 0 {
65                f.write_str(".")?;
66            }
67            f.write_str(segment)?;
68        }
69        if !self.path.is_empty() {
70            f.write_str(": ")?;
71        }
72        self.source.fmt(f)
73    }
74}
75
76impl Error for ConversionError {
77    fn source(&self) -> Option<&(dyn Error + 'static)> {
78        Some(&*self.source)
79    }
80}
81
82#[derive(Debug)]
83struct StringError(String);
84
85impl fmt::Display for StringError {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.write_str(&self.0)
88    }
89}
90
91impl Error for StringError {}
92
93#[derive(Debug)]
94struct ContextualError {
95    message: String,
96    source: Box<dyn Error + Send + Sync>,
97}
98
99impl fmt::Display for ContextualError {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.write_str(&self.message)
102    }
103}
104
105impl Error for ContextualError {
106    fn source(&self) -> Option<&(dyn Error + 'static)> {
107        Some(&*self.source)
108    }
109}
110
111pub trait ConversionResultExt<T> {
112    /// Adds a field path segment if the result is an error.
113    fn context(self, field: impl Into<String>) -> Result<T, ConversionError>;
114
115    /// Computes and adds a field path segment only if the result is an error.
116    fn with_context<F, S>(self, field: F) -> Result<T, ConversionError>
117    where
118        F: FnOnce() -> S,
119        S: Into<String>;
120}
121
122impl<T, E> ConversionResultExt<T> for Result<T, E>
123where
124    E: Error + Send + Sync + 'static,
125{
126    fn context(self, field: impl Into<String>) -> Result<T, ConversionError> {
127        self.with_context(|| field)
128    }
129
130    fn with_context<F, S>(self, field: F) -> Result<T, ConversionError>
131    where
132        F: FnOnce() -> S,
133        S: Into<String>,
134    {
135        self.map_err(|error| ConversionError::new(error).context(field()))
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use alloc::string::ToString;
142    use core::error::Error;
143    use core::num::TryFromIntError;
144
145    use super::{ConversionError, ConversionResultExt};
146
147    #[test]
148    fn with_context_does_not_evaluate_the_closure_on_success() {
149        let value = Ok::<_, TryFromIntError>(7)
150            .with_context(|| -> &'static str { panic!("context must not be evaluated") })
151            .unwrap();
152
153        assert_eq!(value, 7);
154    }
155
156    #[test]
157    fn with_context_evaluates_once_and_preserves_the_path_and_source() {
158        let source = u8::try_from(256_u16).unwrap_err();
159        let inner = ConversionError::new(source).context("inner");
160        let field = "outer".to_string();
161        let mut calls = 0;
162
163        let error = Err::<(), _>(inner)
164            .with_context(|| {
165                calls += 1;
166                field
167            })
168            .unwrap_err();
169
170        assert_eq!(calls, 1);
171        assert_eq!(error.to_string(), alloc::format!("outer.inner: {source}"));
172        assert!(error.source().unwrap().is::<TryFromIntError>());
173    }
174
175    #[test]
176    fn deserialization_errors_preserve_the_source() {
177        let source = u8::try_from(256_u16).unwrap_err();
178        let error = ConversionError::deserialization("Payload", source);
179        assert_eq!(error.to_string(), alloc::format!("failed to deserialize Payload: {source}"));
180        assert!(error.source().unwrap().source().unwrap().is::<TryFromIntError>());
181    }
182
183    #[test]
184    fn wrapping_a_conversion_error_preserves_its_path() {
185        let inner = ConversionError::message("invalid value").context("inner");
186        let outer = ConversionError::new(inner).context("outer");
187
188        assert_eq!(outer.to_string(), "outer.inner: invalid value");
189    }
190}