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}