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 wrong_variant(expected: &'static str, actual: &'static str) -> Self {
40 Self::message(format!("expected oneof variant `{expected}`, got `{actual}`"))
41 .context(actual)
42 }
43
44 pub fn deserialization(
45 entity: &'static str,
46 source: impl Error + Send + Sync + 'static,
47 ) -> Self {
48 let message = format!("failed to deserialize {entity}: {source}");
49 Self::with_source(message, source)
50 }
51
52 pub fn message(message: impl Into<String>) -> Self {
53 Self {
54 path: Vec::new(),
55 source: Box::new(StringError(message.into())),
56 }
57 }
58
59 pub fn with_source(
60 message: impl Into<String>,
61 source: impl Error + Send + Sync + 'static,
62 ) -> Self {
63 Self::new(ContextualError {
64 message: message.into(),
65 source: Box::new(source),
66 })
67 }
68
69 #[cfg(feature = "tonic")]
78 pub fn into_status(self) -> tonic::Status {
79 use alloc::string::ToString;
80 use alloc::sync::Arc;
81 use core::fmt::Write;
82
83 let mut message = self.to_string();
84 let mut cause = self.source.source();
86 while let Some(error) = cause {
87 write!(message, "\ncaused by: {error}").expect("writing to a String cannot fail");
88 cause = error.source();
89 }
90 let mut status = tonic::Status::invalid_argument(message);
91 status.set_source(Arc::new(self));
92 status
93 }
94}
95
96impl fmt::Display for ConversionError {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 for (index, segment) in self.path.iter().rev().enumerate() {
99 if index > 0 {
100 f.write_str(".")?;
101 }
102 f.write_str(segment)?;
103 }
104 if !self.path.is_empty() {
105 f.write_str(": ")?;
106 }
107 self.source.fmt(f)
108 }
109}
110
111impl Error for ConversionError {
112 fn source(&self) -> Option<&(dyn Error + 'static)> {
113 Some(&*self.source)
114 }
115}
116
117#[derive(Debug)]
118struct StringError(String);
119
120impl fmt::Display for StringError {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 f.write_str(&self.0)
123 }
124}
125
126impl Error for StringError {}
127
128#[derive(Debug)]
129struct ContextualError {
130 message: String,
131 source: Box<dyn Error + Send + Sync>,
132}
133
134impl fmt::Display for ContextualError {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.write_str(&self.message)
137 }
138}
139
140impl Error for ContextualError {
141 fn source(&self) -> Option<&(dyn Error + 'static)> {
142 Some(&*self.source)
143 }
144}
145
146pub trait ConversionResultExt<T> {
147 fn context(self, field: impl Into<String>) -> Result<T, ConversionError>;
149
150 fn with_context<F, S>(self, field: F) -> Result<T, ConversionError>
152 where
153 F: FnOnce() -> S,
154 S: Into<String>;
155}
156
157impl<T, E> ConversionResultExt<T> for Result<T, E>
158where
159 E: Error + Send + Sync + 'static,
160{
161 fn context(self, field: impl Into<String>) -> Result<T, ConversionError> {
162 self.with_context(|| field)
163 }
164
165 fn with_context<F, S>(self, field: F) -> Result<T, ConversionError>
166 where
167 F: FnOnce() -> S,
168 S: Into<String>,
169 {
170 self.map_err(|error| ConversionError::new(error).context(field()))
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use alloc::string::ToString;
177 use core::error::Error;
178 use core::num::TryFromIntError;
179
180 use super::{ConversionError, ConversionResultExt};
181
182 #[test]
183 fn with_context_does_not_evaluate_the_closure_on_success() {
184 let value = Ok::<_, TryFromIntError>(7)
185 .with_context(|| -> &'static str { panic!("context must not be evaluated") })
186 .unwrap();
187
188 assert_eq!(value, 7);
189 }
190
191 #[test]
192 fn with_context_evaluates_once_and_preserves_the_path_and_source() {
193 let source = u8::try_from(256_u16).unwrap_err();
194 let inner = ConversionError::new(source).context("inner");
195 let field = "outer".to_string();
196 let mut calls = 0;
197
198 let error = Err::<(), _>(inner)
199 .with_context(|| {
200 calls += 1;
201 field
202 })
203 .unwrap_err();
204
205 assert_eq!(calls, 1);
206 assert_eq!(error.to_string(), alloc::format!("outer.inner: {source}"));
207 assert!(error.source().unwrap().is::<TryFromIntError>());
208 }
209
210 #[test]
211 fn deserialization_errors_preserve_the_source() {
212 let source = u8::try_from(256_u16).unwrap_err();
213 let error = ConversionError::deserialization("Payload", source);
214 assert_eq!(error.to_string(), alloc::format!("failed to deserialize Payload: {source}"));
215 assert!(error.source().unwrap().source().unwrap().is::<TryFromIntError>());
216 }
217
218 #[test]
219 fn wrapping_a_conversion_error_preserves_its_path() {
220 let inner = ConversionError::message("invalid value").context("inner");
221 let outer = ConversionError::new(inner).context("outer");
222
223 assert_eq!(outer.to_string(), "outer.inner: invalid value");
224 }
225}