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#[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 fn context(self, field: impl Into<String>) -> Result<T, ConversionError>;
114
115 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}