Skip to main content

remendo/config/
error.rs

1//! Configuration error and warning types.
2
3use std::fmt;
4
5/// Errors that can occur during configuration loading.
6#[derive(Debug)]
7pub enum ConfigError {
8    /// Filesystem I/O error reading the config file.
9    Io(std::io::Error),
10    /// TOML parsing/deserialization error.
11    Parse(toml::de::Error),
12    /// Validation errors after successful parsing.
13    Validation(Vec<String>),
14}
15
16impl fmt::Display for ConfigError {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::Io(e) => write!(f, "config I/O error: {e}"),
20            Self::Parse(e) => write!(f, "config parse error: {e}"),
21            Self::Validation(msgs) => {
22                write!(f, "config validation errors: ")?;
23                for (i, msg) in msgs.iter().enumerate() {
24                    if i > 0 {
25                        write!(f, "; ")?;
26                    }
27                    write!(f, "{msg}")?;
28                }
29                Ok(())
30            }
31        }
32    }
33}
34
35impl std::error::Error for ConfigError {
36    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
37        match self {
38            Self::Io(e) => Some(e),
39            Self::Parse(e) => Some(e),
40            Self::Validation(_) => None,
41        }
42    }
43}
44
45impl From<std::io::Error> for ConfigError {
46    fn from(e: std::io::Error) -> Self {
47        Self::Io(e)
48    }
49}
50
51impl From<toml::de::Error> for ConfigError {
52    fn from(e: toml::de::Error) -> Self {
53        Self::Parse(e)
54    }
55}
56
57/// Non-fatal warnings collected during configuration loading.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum ConfigWarning {
60    /// No config file was found; using compiled-in defaults.
61    NoFile,
62    /// A remote URL could not be parsed.
63    InvalidRemoteUrl {
64        /// Name of the misconfigured remote.
65        name: String,
66        /// The invalid URL string.
67        url: String,
68    },
69    /// A non-critical parse issue.
70    ParseWarning(String),
71}
72
73impl fmt::Display for ConfigWarning {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::NoFile => write!(f, "no config file found, using defaults"),
77            Self::InvalidRemoteUrl { name, url } => {
78                write!(f, "remote '{name}' has invalid URL: {url}")
79            }
80            Self::ParseWarning(msg) => write!(f, "config warning: {msg}"),
81        }
82    }
83}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn config_error_display_io() {
92        let err = ConfigError::Io(std::io::Error::new(
93            std::io::ErrorKind::NotFound,
94            "not found",
95        ));
96        let s = err.to_string();
97        assert!(s.contains("config I/O error"), "got: {s}");
98    }
99
100    #[test]
101    fn config_error_display_parse() {
102        // Create a real TOML parse error
103        let toml_err = toml::from_str::<toml::Value>("{{bad").unwrap_err();
104        let err = ConfigError::Parse(toml_err);
105        let s = err.to_string();
106        assert!(s.contains("config parse error"), "got: {s}");
107    }
108
109    #[test]
110    fn config_error_display_validation() {
111        let err =
112            ConfigError::Validation(vec!["missing url".to_string(), "bad timeout".to_string()]);
113        let s = err.to_string();
114        assert!(s.contains("config validation errors"), "got: {s}");
115        assert!(s.contains("missing url"), "got: {s}");
116        assert!(s.contains("; bad timeout"), "got: {s}");
117    }
118
119    #[test]
120    fn config_error_display_validation_single() {
121        let err = ConfigError::Validation(vec!["one error".to_string()]);
122        let s = err.to_string();
123        assert!(!s.contains(';'), "single error should not contain ';': {s}");
124    }
125
126    #[test]
127    fn config_error_source_io() {
128        let err = ConfigError::Io(std::io::Error::other("test"));
129        assert!(std::error::Error::source(&err).is_some());
130    }
131
132    #[test]
133    fn config_error_source_parse() {
134        let toml_err = toml::from_str::<toml::Value>("{{bad").unwrap_err();
135        let err = ConfigError::Parse(toml_err);
136        assert!(std::error::Error::source(&err).is_some());
137    }
138
139    #[test]
140    fn config_error_source_validation_is_none() {
141        let err = ConfigError::Validation(vec![]);
142        assert!(std::error::Error::source(&err).is_none());
143    }
144
145    #[test]
146    fn config_warning_display_no_file() {
147        let w = ConfigWarning::NoFile;
148        assert_eq!(w.to_string(), "no config file found, using defaults");
149    }
150
151    #[test]
152    fn config_warning_display_invalid_url() {
153        let w = ConfigWarning::InvalidRemoteUrl {
154            name: "prod".to_string(),
155            url: "not-a-url".to_string(),
156        };
157        let s = w.to_string();
158        assert!(s.contains("prod"), "got: {s}");
159        assert!(s.contains("not-a-url"), "got: {s}");
160    }
161
162    #[test]
163    fn config_warning_display_parse_warning() {
164        let w = ConfigWarning::ParseWarning("something odd".to_string());
165        assert!(w.to_string().contains("something odd"));
166    }
167}