Skip to main content

rust_config_tree/
error.rs

1//! Error types shared by the tree loader and high-level config API.
2//!
3//! The lower-level API reports [`ConfigTreeError`]. The high-level `confique`
4//! integration wraps those traversal failures together with dotenv loading,
5//! Figment extraction, config parsing, schema serialization, and IO errors in
6//! [`ConfigError`].
7
8use std::{
9    error::Error,
10    fmt, io,
11    path::{Path, PathBuf},
12};
13
14/// Boxed error type used by custom loaders.
15///
16/// Loader errors are boxed so tree traversal can accept different concrete
17/// error types through a single public API.
18pub type BoxError = Box<dyn Error + Send + Sync + 'static>;
19
20/// Result type used by the lower-level tree API.
21///
22/// The error type is [`ConfigTreeError`].
23pub type Result<T> = std::result::Result<T, ConfigTreeError>;
24
25/// Errors produced while traversing a recursive config tree.
26#[derive(Debug)]
27pub enum ConfigTreeError {
28    /// The current directory could not be resolved while absolutizing a path.
29    CurrentDir {
30        /// Underlying IO error returned while reading the current directory.
31        source: io::Error,
32    },
33    /// A source loader failed for the given path.
34    Load {
35        /// Path that failed to load.
36        path: PathBuf,
37        /// Underlying loader error.
38        source: BoxError,
39    },
40    /// An include list contained an empty path.
41    EmptyIncludePath {
42        /// Path whose include list contained the empty entry.
43        path: PathBuf,
44        /// Zero-based index of the empty include entry.
45        index: usize,
46    },
47    /// Recursive includes formed a cycle.
48    IncludeCycle {
49        /// Normalized path chain that forms the include cycle.
50        chain: Vec<PathBuf>,
51    },
52}
53
54/// Convenience constructors for tree traversal errors.
55impl ConfigTreeError {
56    /// Builds a loader failure for a source path.
57    ///
58    /// # Type Parameters
59    ///
60    /// - `E`: Concrete error type returned by the source loader.
61    ///
62    /// # Arguments
63    ///
64    /// - `path`: Source path that failed to load.
65    /// - `source`: Underlying loader error.
66    ///
67    /// # Returns
68    ///
69    /// Returns a [`ConfigTreeError::Load`] value.
70    ///
71    /// # Examples
72    ///
73    /// ```no_run
74    /// let _ = ();
75    /// ```
76    pub(crate) fn load<E>(path: &Path, source: E) -> Self
77    where
78        E: Into<BoxError>,
79    {
80        Self::Load {
81            path: path.to_path_buf(),
82            source: source.into(),
83        }
84    }
85}
86
87/// Formats tree traversal errors for CLI and library callers.
88impl fmt::Display for ConfigTreeError {
89    /// Formats one tree traversal error for display.
90    ///
91    /// # Arguments
92    ///
93    /// - `self`: Error value to format.
94    /// - `f`: Formatter receiving the rendered message.
95    ///
96    /// # Returns
97    ///
98    /// Returns the formatter result.
99    ///
100    /// # Examples
101    ///
102    /// ```no_run
103    /// let _ = ();
104    /// ```
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            Self::CurrentDir { .. } => write!(f, "failed to resolve current directory"),
108            Self::Load { path, source } => {
109                write!(f, "failed to load config {}: {source}", path.display())
110            }
111            Self::EmptyIncludePath { path, index } => write!(
112                f,
113                "include path at index {index} in {} must not be empty",
114                path.display()
115            ),
116            Self::IncludeCycle { chain } => {
117                let chain = chain
118                    .iter()
119                    .map(|path| path.display().to_string())
120                    .collect::<Vec<_>>()
121                    .join(" -> ");
122                write!(f, "recursive config include cycle: {chain}")
123            }
124        }
125    }
126}
127
128/// Exposes underlying IO or loader causes for tree traversal failures.
129impl Error for ConfigTreeError {
130    /// Returns the underlying source error when one exists.
131    ///
132    /// # Arguments
133    ///
134    /// - `self`: Error value whose source should be exposed.
135    ///
136    /// # Returns
137    ///
138    /// Returns the wrapped source error for IO or loader failures.
139    ///
140    /// # Examples
141    ///
142    /// ```no_run
143    /// let _ = ();
144    /// ```
145    fn source(&self) -> Option<&(dyn Error + 'static)> {
146        match self {
147            Self::CurrentDir { source } => Some(source),
148            Self::Load { source, .. } => Some(source.as_ref()),
149            Self::EmptyIncludePath { .. } | Self::IncludeCycle { .. } => None,
150        }
151    }
152}
153
154/// Errors produced by high-level config loading and template generation.
155#[derive(Debug)]
156pub enum ConfigError {
157    /// Tree traversal failed.
158    Tree(Box<ConfigTreeError>),
159    /// Loading an existing `.env` file failed.
160    Dotenv(Box<dotenvy::Error>),
161    /// Figment failed to load or deserialize runtime config data.
162    Figment(Box<figment::Error>),
163    /// `confique` failed to load or merge config data.
164    Config(Box<confique::Error>),
165    /// JSON schema serialization failed.
166    Json(Box<serde_json::Error>),
167    /// File system or shell completion IO failed.
168    Io(Box<io::Error>),
169}
170
171/// Formats high-level config errors by delegating to their underlying causes.
172impl fmt::Display for ConfigError {
173    /// Formats one high-level config error for display.
174    ///
175    /// # Arguments
176    ///
177    /// - `self`: Error value to format.
178    /// - `f`: Formatter receiving the rendered message.
179    ///
180    /// # Returns
181    ///
182    /// Returns the formatter result.
183    ///
184    /// # Examples
185    ///
186    /// ```no_run
187    /// let _ = ();
188    /// ```
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        match self {
191            Self::Tree(err) => err.fmt(f),
192            Self::Dotenv(err) => err.fmt(f),
193            Self::Figment(err) => err.fmt(f),
194            Self::Config(err) => err.fmt(f),
195            Self::Json(err) => err.fmt(f),
196            Self::Io(err) => err.fmt(f),
197        }
198    }
199}
200
201/// Exposes the wrapped source error for high-level config failures.
202impl Error for ConfigError {
203    /// Returns the wrapped high-level source error.
204    ///
205    /// # Arguments
206    ///
207    /// - `self`: Error value whose source should be exposed.
208    ///
209    /// # Returns
210    ///
211    /// Returns the wrapped source error for this variant.
212    ///
213    /// # Examples
214    ///
215    /// ```no_run
216    /// let _ = ();
217    /// ```
218    fn source(&self) -> Option<&(dyn Error + 'static)> {
219        match self {
220            Self::Tree(err) => Some(err.as_ref()),
221            Self::Dotenv(err) => Some(err.as_ref()),
222            Self::Figment(err) => Some(err.as_ref()),
223            Self::Config(err) => Some(err.as_ref()),
224            Self::Json(err) => Some(err.as_ref()),
225            Self::Io(err) => Some(err.as_ref()),
226        }
227    }
228}
229
230/// Converts tree traversal failures into high-level config failures.
231impl From<ConfigTreeError> for ConfigError {
232    /// Wraps a tree traversal error.
233    ///
234    /// # Arguments
235    ///
236    /// - `err`: Tree traversal error to wrap.
237    ///
238    /// # Returns
239    ///
240    /// Returns the corresponding high-level config error.
241    ///
242    /// # Examples
243    ///
244    /// ```no_run
245    /// let _ = ();
246    /// ```
247    fn from(err: ConfigTreeError) -> Self {
248        Self::Tree(Box::new(err))
249    }
250}
251
252/// Converts dotenv loading failures into high-level config failures.
253impl From<dotenvy::Error> for ConfigError {
254    /// Wraps a dotenv loading error.
255    ///
256    /// # Arguments
257    ///
258    /// - `err`: Dotenv error to wrap.
259    ///
260    /// # Returns
261    ///
262    /// Returns the corresponding high-level config error.
263    ///
264    /// # Examples
265    ///
266    /// ```no_run
267    /// let _ = ();
268    /// ```
269    fn from(err: dotenvy::Error) -> Self {
270        Self::Dotenv(Box::new(err))
271    }
272}
273
274/// Converts Figment extraction failures into high-level config failures.
275impl From<figment::Error> for ConfigError {
276    /// Wraps a Figment extraction error.
277    ///
278    /// # Arguments
279    ///
280    /// - `err`: Figment error to wrap.
281    ///
282    /// # Returns
283    ///
284    /// Returns the corresponding high-level config error.
285    ///
286    /// # Examples
287    ///
288    /// ```no_run
289    /// let _ = ();
290    /// ```
291    fn from(err: figment::Error) -> Self {
292        Self::Figment(Box::new(err))
293    }
294}
295
296/// Converts `confique` merge failures into high-level config failures.
297impl From<confique::Error> for ConfigError {
298    /// Wraps a `confique` merge or validation error.
299    ///
300    /// # Arguments
301    ///
302    /// - `err`: `confique` error to wrap.
303    ///
304    /// # Returns
305    ///
306    /// Returns the corresponding high-level config error.
307    ///
308    /// # Examples
309    ///
310    /// ```no_run
311    /// let _ = ();
312    /// ```
313    fn from(err: confique::Error) -> Self {
314        Self::Config(Box::new(err))
315    }
316}
317
318/// Converts JSON serialization failures into high-level config failures.
319impl From<serde_json::Error> for ConfigError {
320    /// Wraps a JSON serialization error.
321    ///
322    /// # Arguments
323    ///
324    /// - `err`: JSON error to wrap.
325    ///
326    /// # Returns
327    ///
328    /// Returns the corresponding high-level config error.
329    ///
330    /// # Examples
331    ///
332    /// ```no_run
333    /// let _ = ();
334    /// ```
335    fn from(err: serde_json::Error) -> Self {
336        Self::Json(Box::new(err))
337    }
338}
339
340/// Converts IO failures into high-level config failures.
341impl From<io::Error> for ConfigError {
342    /// Wraps an IO error.
343    ///
344    /// # Arguments
345    ///
346    /// - `err`: IO error to wrap.
347    ///
348    /// # Returns
349    ///
350    /// Returns the corresponding high-level config error.
351    ///
352    /// # Examples
353    ///
354    /// ```no_run
355    /// let _ = ();
356    /// ```
357    fn from(err: io::Error) -> Self {
358        Self::Io(Box::new(err))
359    }
360}