config/
error.rs

1use std::error::Error;
2use std::fmt;
3use std::result;
4
5use serde::de;
6use serde::ser;
7
8#[allow(unnameable_types)] // Unsure if/how to expose this
9#[derive(Debug)]
10pub enum Unexpected {
11    Bool(bool),
12    I64(i64),
13    I128(i128),
14    U64(u64),
15    U128(u128),
16    Float(f64),
17    Str(String),
18    Unit,
19    Seq,
20    Map,
21}
22
23impl fmt::Display for Unexpected {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> result::Result<(), fmt::Error> {
25        match *self {
26            Unexpected::Bool(b) => write!(f, "boolean `{b}`"),
27            Unexpected::I64(i) => write!(f, "64-bit integer `{i}`"),
28            Unexpected::I128(i) => write!(f, "128-bit integer `{i}`"),
29            Unexpected::U64(i) => write!(f, "64-bit unsigned integer `{i}`"),
30            Unexpected::U128(i) => write!(f, "128-bit unsigned integer `{i}`"),
31            Unexpected::Float(v) => write!(f, "floating point `{v}`"),
32            Unexpected::Str(ref s) => write!(f, "string {s:?}"),
33            Unexpected::Unit => write!(f, "unit value"),
34            Unexpected::Seq => write!(f, "sequence"),
35            Unexpected::Map => write!(f, "map"),
36        }
37    }
38}
39
40/// Represents all possible errors that can occur when working with
41/// configuration.
42#[non_exhaustive]
43pub enum ConfigError {
44    /// Configuration is frozen and no further mutations can be made.
45    Frozen,
46
47    /// Configuration property was not found
48    NotFound(String),
49
50    /// Configuration path could not be parsed.
51    PathParse { cause: Box<dyn Error + Send + Sync> },
52
53    /// Configuration could not be parsed from file.
54    FileParse {
55        /// The URI used to access the file (if not loaded from a string).
56        /// Example: `/path/to/config.json`
57        uri: Option<String>,
58
59        /// The captured error from attempting to parse the file in its desired format.
60        /// This is the actual error object from the library used for the parsing.
61        cause: Box<dyn Error + Send + Sync>,
62    },
63
64    /// Value could not be converted into the requested type.
65    Type {
66        /// The URI that references the source that the value came from.
67        /// Example: `/path/to/config.json` or `Environment` or `etcd://localhost`
68        // TODO: Why is this called Origin but FileParse has a uri field?
69        origin: Option<String>,
70
71        /// What we found when parsing the value
72        unexpected: Unexpected,
73
74        /// What was expected when parsing the value
75        expected: &'static str,
76
77        /// The key in the configuration hash of this value (if available where the
78        /// error is generated).
79        key: Option<String>,
80    },
81
82    /// Custom message
83    At {
84        /// Error being extended with a path
85        error: Box<ConfigError>,
86
87        /// The URI that references the source that the value came from.
88        /// Example: `/path/to/config.json` or `Environment` or `etcd://localhost`
89        // TODO: Why is this called Origin but FileParse has a uri field?
90        origin: Option<String>,
91
92        /// The key in the configuration hash of this value (if available where the
93        /// error is generated).
94        key: Option<String>,
95    },
96
97    /// Custom message
98    Message(String),
99
100    /// Unadorned error from a foreign origin.
101    Foreign(Box<dyn Error + Send + Sync>),
102}
103
104impl ConfigError {
105    // FIXME: pub(crate)
106    #[doc(hidden)]
107    pub fn invalid_type(
108        origin: Option<String>,
109        unexpected: Unexpected,
110        expected: &'static str,
111    ) -> Self {
112        Self::Type {
113            origin,
114            unexpected,
115            expected,
116            key: None,
117        }
118    }
119
120    // Have a proper error fire if the root of a file is ever not a Table
121    // TODO: for now only json5 checked, need to finish others
122    #[doc(hidden)]
123    pub fn invalid_root(origin: Option<&String>, unexpected: Unexpected) -> Box<Self> {
124        Box::new(Self::Type {
125            origin: origin.cloned(),
126            unexpected,
127            expected: "a map",
128            key: None,
129        })
130    }
131
132    // FIXME: pub(crate)
133    #[doc(hidden)]
134    #[must_use]
135    pub fn extend_with_key(self, key: &str) -> Self {
136        match self {
137            Self::Type {
138                origin,
139                unexpected,
140                expected,
141                ..
142            } => Self::Type {
143                origin,
144                unexpected,
145                expected,
146                key: Some(key.into()),
147            },
148
149            Self::At { origin, error, .. } => Self::At {
150                error,
151                origin,
152                key: Some(key.into()),
153            },
154
155            other => Self::At {
156                error: Box::new(other),
157                origin: None,
158                key: Some(key.into()),
159            },
160        }
161    }
162
163    #[must_use]
164    fn prepend(self, segment: &str, add_dot: bool) -> Self {
165        let concat = |key: Option<String>| {
166            let key = key.unwrap_or_default();
167            let dot = if add_dot && key.as_bytes().first().unwrap_or(&b'[') != &b'[' {
168                "."
169            } else {
170                ""
171            };
172            format!("{segment}{dot}{key}")
173        };
174        match self {
175            Self::Type {
176                origin,
177                unexpected,
178                expected,
179                key,
180            } => Self::Type {
181                origin,
182                unexpected,
183                expected,
184                key: Some(concat(key)),
185            },
186            Self::At { error, origin, key } => Self::At {
187                error,
188                origin,
189                key: Some(concat(key)),
190            },
191            Self::NotFound(key) => Self::NotFound(concat(Some(key))),
192            other => Self::At {
193                error: Box::new(other),
194                origin: None,
195                key: Some(concat(None)),
196            },
197        }
198    }
199
200    #[must_use]
201    pub(crate) fn prepend_key(self, key: &str) -> Self {
202        self.prepend(key, true)
203    }
204
205    #[must_use]
206    pub(crate) fn prepend_index(self, idx: usize) -> Self {
207        self.prepend(&format!("[{idx}]"), false)
208    }
209}
210
211/// Alias for a `Result` with the error type set to `ConfigError`.
212pub(crate) type Result<T, E = ConfigError> = result::Result<T, E>;
213
214// Forward Debug to Display for readable panic! messages
215impl fmt::Debug for ConfigError {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        write!(f, "{}", *self)
218    }
219}
220
221impl fmt::Display for ConfigError {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match *self {
224            ConfigError::Frozen => write!(f, "configuration is frozen"),
225
226            ConfigError::PathParse { ref cause } => write!(f, "{cause}"),
227
228            ConfigError::Message(ref s) => write!(f, "{s}"),
229
230            ConfigError::Foreign(ref cause) => write!(f, "{cause}"),
231
232            ConfigError::NotFound(ref key) => {
233                write!(f, "configuration property {key:?} not found")
234            }
235
236            ConfigError::Type {
237                ref origin,
238                ref unexpected,
239                expected,
240                ref key,
241            } => {
242                write!(f, "invalid type: {unexpected}, expected {expected}")?;
243
244                if let Some(ref key) = *key {
245                    write!(f, " for key `{key}`")?;
246                }
247
248                if let Some(ref origin) = *origin {
249                    write!(f, " in {origin}")?;
250                }
251
252                Ok(())
253            }
254
255            ConfigError::At {
256                ref error,
257                ref origin,
258                ref key,
259            } => {
260                write!(f, "{error}")?;
261
262                if let Some(ref key) = *key {
263                    write!(f, " for key `{key}`")?;
264                }
265
266                if let Some(ref origin) = *origin {
267                    write!(f, " in {origin}")?;
268                }
269
270                Ok(())
271            }
272
273            ConfigError::FileParse { ref cause, ref uri } => {
274                write!(f, "{cause}")?;
275
276                if let Some(ref uri) = *uri {
277                    write!(f, " in {uri}")?;
278                }
279
280                Ok(())
281            }
282        }
283    }
284}
285
286impl Error for ConfigError {}
287
288impl de::Error for ConfigError {
289    fn custom<T: fmt::Display>(msg: T) -> Self {
290        Self::Message(msg.to_string())
291    }
292}
293
294impl ser::Error for ConfigError {
295    fn custom<T: fmt::Display>(msg: T) -> Self {
296        Self::Message(msg.to_string())
297    }
298}