1use std::fmt;
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum ErrorKind {
17 Io,
19 Parse,
21 Missing,
23 Type,
25 Env,
27 Invalid,
29 Remote,
31 Decrypt,
33 Backend,
35}
36
37impl ErrorKind {
38 #[must_use]
40 pub fn as_str(self) -> &'static str {
41 match self {
42 Self::Io => "io",
43 Self::Parse => "parse",
44 Self::Missing => "missing",
45 Self::Type => "type",
46 Self::Env => "env",
47 Self::Invalid => "invalid",
48 Self::Remote => "remote",
49 Self::Decrypt => "decrypt",
50 Self::Backend => "backend",
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
60#[non_exhaustive]
61pub enum Origin {
62 File(PathBuf),
64 Env(String),
66 Inline,
68 Remote(String),
70 Runtime(&'static str),
72 Unknown,
74}
75
76impl fmt::Display for Origin {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::File(path) => write!(f, "in {}", path.display()),
80 Self::Env(name) => write!(f, "from {name}"),
81 Self::Inline => f.write_str("in an inline source"),
82 Self::Remote(store) => write!(f, "from {store}"),
83 Self::Runtime(layer) => write!(f, "set as {layer}"),
84 Self::Unknown => f.write_str("origin unknown"),
85 }
86 }
87}
88
89pub struct Error {
94 inner: Box<Inner>,
95}
96
97struct Inner {
98 kind: ErrorKind,
99 path: Vec<String>,
101 origin: Origin,
102 message: String,
103}
104
105impl Error {
106 pub(crate) fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
108 Self {
109 inner: Box::new(Inner {
110 kind,
111 path: Vec::new(),
112 origin: Origin::Unknown,
113 message: message.into(),
114 }),
115 }
116 }
117
118 pub fn invalid<E: fmt::Display>(error: E) -> Self {
123 Self::new(ErrorKind::Invalid, error.to_string())
124 }
125
126 pub fn ok_or_invalid<T, E: fmt::Display>(outcome: Result<T, E>) -> Result<(), Self> {
135 outcome.map(|_| ()).map_err(Self::invalid)
136 }
137
138 pub fn remote<E: fmt::Display>(error: E) -> Self {
143 Self::new(ErrorKind::Remote, error.to_string())
144 }
145
146 pub fn decrypt<E: fmt::Display>(error: E) -> Self {
151 Self::new(ErrorKind::Decrypt, error.to_string())
152 }
153
154 #[must_use]
156 pub fn unsupported(path: &std::path::Path) -> Self {
157 Self::new(
158 ErrorKind::Backend,
159 "the extension names no supported format; expected `.json`, `.toml`, \
160 `.yaml` or `.yml`",
161 )
162 .with_origin(Origin::File(path.to_owned()))
163 }
164
165 #[must_use]
167 pub fn kind(&self) -> ErrorKind {
168 self.inner.kind
169 }
170
171 #[must_use]
173 pub fn path(&self) -> String {
174 self.inner.path.join(".")
175 }
176
177 #[must_use]
179 pub fn origin(&self) -> &Origin {
180 &self.inner.origin
181 }
182
183 #[must_use]
185 pub fn message(&self) -> &str {
186 &self.inner.message
187 }
188
189 pub(crate) fn prepend_key(mut self, key: impl Into<String>) -> Self {
195 self.inner.path.insert(0, key.into());
196 self
197 }
198
199 pub(crate) fn with_origin(mut self, origin: Origin) -> Self {
201 if self.inner.origin == Origin::Unknown {
202 self.inner.origin = origin;
203 }
204 self
205 }
206}
207
208impl fmt::Display for Error {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 if self.inner.path.is_empty() {
211 f.write_str(&self.inner.message)?;
212 } else {
213 write!(f, "{}: {}", self.path(), self.inner.message)?;
214 }
215
216 if self.inner.origin != Origin::Unknown {
217 write!(f, " ({})", self.inner.origin)?;
218 }
219
220 Ok(())
221 }
222}
223
224impl fmt::Debug for Error {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 f.debug_struct("Error")
227 .field("kind", &self.inner.kind)
228 .field("path", &self.path())
229 .field("origin", &self.inner.origin)
230 .field("message", &self.inner.message)
231 .finish()
232 }
233}
234
235impl std::error::Error for Error {}
236
237impl serde::de::Error for Error {
238 fn custom<T: fmt::Display>(msg: T) -> Self {
239 Self::new(ErrorKind::Type, msg.to_string())
240 }
241
242 fn missing_field(field: &'static str) -> Self {
243 Self::new(ErrorKind::Missing, "missing value").prepend_key(field)
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use serde::de::Error as _;
251
252 #[test]
253 fn prepend_key_assembles_the_path_outermost_first() {
254 let error = Error::new(ErrorKind::Type, "boom")
255 .prepend_key("max")
256 .prepend_key("pool")
257 .prepend_key("db");
258
259 assert_eq!(error.path(), "db.pool.max");
260 }
261
262 #[test]
263 fn display_includes_the_path_and_the_origin() {
264 let error = Error::new(ErrorKind::Type, "invalid type")
265 .prepend_key("port")
266 .with_origin(Origin::Env("APP_DB_PORT".to_owned()));
267
268 assert_eq!(error.to_string(), "port: invalid type (from APP_DB_PORT)");
269 }
270
271 #[test]
272 fn display_omits_both_decorations_when_they_are_absent() {
273 let error = Error::new(ErrorKind::Parse, "unexpected end of input");
274
275 assert_eq!(error.to_string(), "unexpected end of input");
276 }
277
278 #[test]
279 fn the_first_origin_recorded_wins() {
280 let error = Error::new(ErrorKind::Type, "boom")
281 .with_origin(Origin::Inline)
282 .with_origin(Origin::Env("APP_X".to_owned()));
283
284 assert_eq!(error.origin(), &Origin::Inline);
285 }
286
287 #[test]
288 fn a_missing_field_carries_its_name_and_kind() {
289 let error = Error::missing_field("host");
290
291 assert_eq!(error.kind(), ErrorKind::Missing);
292 assert_eq!(error.path(), "host");
293 }
294}