1use std::fmt;
4
5#[derive(Debug)]
7pub enum ConfigError {
8 Io(std::io::Error),
10 Parse(toml::de::Error),
12 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#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum ConfigWarning {
60 NoFile,
62 InvalidRemoteUrl {
64 name: String,
66 url: String,
68 },
69 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 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}