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
54impl ConfigTreeError {
55    /// Builds a loader failure for a source path.
56    ///
57    /// # Type Parameters
58    ///
59    /// - `E`: Concrete error type returned by the source loader.
60    ///
61    /// # Arguments
62    ///
63    /// - `path`: Source path that failed to load.
64    /// - `source`: Underlying loader error.
65    ///
66    /// # Returns
67    ///
68    /// Returns a [`ConfigTreeError::Load`] value.
69    pub(crate) fn load<E>(path: &Path, source: E) -> Self
70    where
71        E: Into<BoxError>,
72    {
73        Self::Load {
74            path: path.to_path_buf(),
75            source: source.into(),
76        }
77    }
78}
79
80impl fmt::Display for ConfigTreeError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            Self::CurrentDir { .. } => write!(f, "failed to resolve current directory"),
84            Self::Load { path, source } => {
85                write!(f, "failed to load config {}: {source}", path.display())
86            }
87            Self::EmptyIncludePath { path, index } => write!(
88                f,
89                "include path at index {index} in {} must not be empty",
90                path.display()
91            ),
92            Self::IncludeCycle { chain } => {
93                let chain = chain
94                    .iter()
95                    .map(|path| path.display().to_string())
96                    .collect::<Vec<_>>()
97                    .join(" -> ");
98                write!(f, "recursive config include cycle: {chain}")
99            }
100        }
101    }
102}
103
104impl Error for ConfigTreeError {
105    fn source(&self) -> Option<&(dyn Error + 'static)> {
106        match self {
107            Self::CurrentDir { source } => Some(source),
108            Self::Load { source, .. } => Some(source.as_ref()),
109            Self::EmptyIncludePath { .. } | Self::IncludeCycle { .. } => None,
110        }
111    }
112}
113
114/// Errors produced by high-level config loading and template generation.
115#[derive(Debug)]
116pub enum ConfigError {
117    /// Tree traversal failed.
118    Tree(Box<ConfigTreeError>),
119    /// Loading an existing `.env` file failed.
120    Dotenv(Box<dotenvy::Error>),
121    /// Figment failed to load or deserialize runtime config data.
122    Figment(Box<figment::Error>),
123    /// `confique` failed to load or merge config data.
124    Config(Box<confique::Error>),
125    /// JSON schema serialization failed.
126    Json(Box<serde_json::Error>),
127    /// File system or shell completion IO failed.
128    Io(Box<io::Error>),
129}
130
131impl fmt::Display for ConfigError {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            Self::Tree(err) => err.fmt(f),
135            Self::Dotenv(err) => err.fmt(f),
136            Self::Figment(err) => err.fmt(f),
137            Self::Config(err) => err.fmt(f),
138            Self::Json(err) => err.fmt(f),
139            Self::Io(err) => err.fmt(f),
140        }
141    }
142}
143
144impl Error for ConfigError {
145    fn source(&self) -> Option<&(dyn Error + 'static)> {
146        match self {
147            Self::Tree(err) => Some(err.as_ref()),
148            Self::Dotenv(err) => Some(err.as_ref()),
149            Self::Figment(err) => Some(err.as_ref()),
150            Self::Config(err) => Some(err.as_ref()),
151            Self::Json(err) => Some(err.as_ref()),
152            Self::Io(err) => Some(err.as_ref()),
153        }
154    }
155}
156
157impl From<ConfigTreeError> for ConfigError {
158    fn from(err: ConfigTreeError) -> Self {
159        Self::Tree(Box::new(err))
160    }
161}
162
163impl From<dotenvy::Error> for ConfigError {
164    fn from(err: dotenvy::Error) -> Self {
165        Self::Dotenv(Box::new(err))
166    }
167}
168
169impl From<figment::Error> for ConfigError {
170    fn from(err: figment::Error) -> Self {
171        Self::Figment(Box::new(err))
172    }
173}
174
175impl From<confique::Error> for ConfigError {
176    fn from(err: confique::Error) -> Self {
177        Self::Config(Box::new(err))
178    }
179}
180
181impl From<serde_json::Error> for ConfigError {
182    fn from(err: serde_json::Error) -> Self {
183        Self::Json(Box::new(err))
184    }
185}
186
187impl From<io::Error> for ConfigError {
188    fn from(err: io::Error) -> Self {
189        Self::Io(Box::new(err))
190    }
191}