1#[derive(Debug, Clone)]
9pub struct ThemeResolutionError {
10 pub missing_fields: Vec<String>,
12}
13
14impl std::fmt::Display for ThemeResolutionError {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 write!(
17 f,
18 "theme resolution failed: {} missing field(s):",
19 self.missing_fields.len()
20 )?;
21 for field in &self.missing_fields {
22 write!(f, "\n - {field}")?;
23 }
24 Ok(())
25 }
26}
27
28impl std::error::Error for ThemeResolutionError {}
29
30#[derive(Debug)]
32#[non_exhaustive]
33pub enum Error {
34 Unsupported(&'static str),
36
37 Unavailable(String),
39
40 Format(String),
42
43 Platform(Box<dyn std::error::Error + Send + Sync>),
45
46 Io(std::io::Error),
48
49 Resolution(ThemeResolutionError),
51}
52
53impl std::fmt::Display for Error {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Error::Unsupported(reason) => write!(f, "not supported: {reason}"),
57 Error::Unavailable(msg) => write!(f, "theme data unavailable: {msg}"),
58 Error::Format(msg) => write!(f, "theme format error: {msg}"),
59 Error::Platform(err) => write!(f, "platform error: {err}"),
60 Error::Io(err) => write!(f, "I/O error: {err}"),
61 Error::Resolution(e) => write!(f, "{e}"),
62 }
63 }
64}
65
66impl std::error::Error for Error {
67 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
68 match self {
69 Error::Platform(err) => Some(&**err),
70 Error::Io(err) => Some(err),
71 Error::Resolution(e) => Some(e),
72 _ => None,
73 }
74 }
75}
76
77impl From<toml::de::Error> for Error {
78 fn from(err: toml::de::Error) -> Self {
79 Error::Format(err.to_string())
80 }
81}
82
83impl From<toml::ser::Error> for Error {
84 fn from(err: toml::ser::Error) -> Self {
85 Error::Format(err.to_string())
86 }
87}
88
89impl From<std::io::Error> for Error {
90 fn from(err: std::io::Error) -> Self {
91 Error::Io(err)
92 }
93}
94
95#[cfg(test)]
96#[allow(clippy::unwrap_used, clippy::expect_used)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn unsupported_display() {
102 let err = Error::Unsupported("KDE theme detection requires the `kde` feature");
103 let msg = err.to_string();
104 assert!(msg.contains("not supported"), "got: {msg}");
105 assert!(msg.contains("kde"), "got: {msg}");
106 }
107
108 #[test]
109 fn unavailable_display() {
110 let err = Error::Unavailable("file not found".into());
111 let msg = err.to_string();
112 assert!(msg.contains("unavailable"), "got: {msg}");
113 assert!(msg.contains("file not found"), "got: {msg}");
114 }
115
116 #[test]
117 fn format_display() {
118 let err = Error::Format("invalid TOML".into());
119 let msg = err.to_string();
120 assert!(msg.contains("format error"), "got: {msg}");
121 assert!(msg.contains("invalid TOML"), "got: {msg}");
122 }
123
124 #[test]
125 fn platform_display() {
126 let inner = std::io::Error::other("dbus failure");
127 let err = Error::Platform(Box::new(inner));
128 let msg = err.to_string();
129 assert!(msg.contains("platform error"), "got: {msg}");
130 assert!(msg.contains("dbus failure"), "got: {msg}");
131 }
132
133 #[test]
134 fn platform_source_returns_inner() {
135 let inner = std::io::Error::other("inner error");
136 let err = Error::Platform(Box::new(inner));
137 let source = std::error::Error::source(&err);
138 assert!(source.is_some());
139 assert!(source.unwrap().to_string().contains("inner error"));
140 }
141
142 #[test]
143 fn non_platform_source_is_none() {
144 assert!(std::error::Error::source(&Error::Unsupported("test")).is_none());
145 assert!(std::error::Error::source(&Error::Unavailable("x".into())).is_none());
146 assert!(std::error::Error::source(&Error::Format("x".into())).is_none());
147 }
148
149 #[test]
150 fn error_is_send_sync() {
151 fn assert_send_sync<T: Send + Sync>() {}
152 assert_send_sync::<Error>();
153 }
154
155 #[test]
156 fn from_toml_de_error() {
157 let toml_err: Result<toml::Value, toml::de::Error> = toml::from_str("=invalid");
159 let err: Error = toml_err.unwrap_err().into();
160 match &err {
161 Error::Format(msg) => assert!(!msg.is_empty()),
162 other => panic!("expected Format variant, got: {other:?}"),
163 }
164 }
165
166 #[test]
167 fn from_io_error() {
168 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing file");
169 let err: Error = io_err.into();
170 match &err {
171 Error::Io(e) => {
172 assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
173 assert!(e.to_string().contains("missing file"));
174 }
175 other => panic!("expected Io variant, got: {other:?}"),
176 }
177 }
178
179 #[test]
182 fn theme_resolution_error_construction() {
183 let e = ThemeResolutionError {
184 missing_fields: vec!["defaults.accent".into(), "button.font.family".into()],
185 };
186 assert_eq!(e.missing_fields.len(), 2);
187 assert_eq!(e.missing_fields[0], "defaults.accent");
188 assert_eq!(e.missing_fields[1], "button.font.family");
189 }
190
191 #[test]
192 fn theme_resolution_error_display_lists_count_and_fields() {
193 let e = ThemeResolutionError {
194 missing_fields: vec![
195 "defaults.accent".into(),
196 "button.foreground".into(),
197 "window.radius".into(),
198 ],
199 };
200 let msg = e.to_string();
201 assert!(msg.contains("3 missing field(s)"), "got: {msg}");
202 assert!(msg.contains("defaults.accent"), "got: {msg}");
203 assert!(msg.contains("button.foreground"), "got: {msg}");
204 assert!(msg.contains("window.radius"), "got: {msg}");
205 }
206
207 #[test]
208 fn theme_resolution_error_implements_std_error() {
209 let e = ThemeResolutionError {
210 missing_fields: vec!["defaults.accent".into()],
211 };
212 let _: &dyn std::error::Error = &e;
214 }
215
216 #[test]
217 fn theme_resolution_error_is_clone() {
218 let e = ThemeResolutionError {
219 missing_fields: vec!["defaults.accent".into()],
220 };
221 let e2 = e.clone();
222 assert_eq!(e.missing_fields, e2.missing_fields);
223 }
224
225 #[test]
228 fn error_resolution_variant_wraps_theme_resolution_error() {
229 let inner = ThemeResolutionError {
230 missing_fields: vec!["defaults.accent".into()],
231 };
232 let err = Error::Resolution(inner);
233 match &err {
234 Error::Resolution(e) => assert_eq!(e.missing_fields.len(), 1),
235 other => panic!("expected Resolution variant, got: {other:?}"),
236 }
237 }
238
239 #[test]
240 fn error_resolution_display_delegates_to_inner() {
241 let inner = ThemeResolutionError {
242 missing_fields: vec!["defaults.accent".into(), "window.radius".into()],
243 };
244 let err = Error::Resolution(inner);
245 let msg = err.to_string();
246 assert!(msg.contains("2 missing field(s)"), "got: {msg}");
247 assert!(msg.contains("defaults.accent"), "got: {msg}");
248 assert!(msg.contains("window.radius"), "got: {msg}");
249 }
250
251 #[test]
252 fn error_resolution_source_returns_inner() {
253 let inner = ThemeResolutionError {
254 missing_fields: vec!["defaults.accent".into()],
255 };
256 let err = Error::Resolution(inner);
257 let source = std::error::Error::source(&err);
258 assert!(
259 source.is_some(),
260 "source() should return Some for Resolution variant"
261 );
262 let source_msg = source.unwrap().to_string();
263 assert!(
264 source_msg.contains("1 missing field(s)"),
265 "got: {source_msg}"
266 );
267 }
268}