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,
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 => write!(f, "operation not supported on this platform"),
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;
103 let msg = err.to_string();
104 assert!(msg.contains("not supported"), "got: {msg}");
105 }
106
107 #[test]
108 fn unavailable_display() {
109 let err = Error::Unavailable("file not found".into());
110 let msg = err.to_string();
111 assert!(msg.contains("unavailable"), "got: {msg}");
112 assert!(msg.contains("file not found"), "got: {msg}");
113 }
114
115 #[test]
116 fn format_display() {
117 let err = Error::Format("invalid TOML".into());
118 let msg = err.to_string();
119 assert!(msg.contains("format error"), "got: {msg}");
120 assert!(msg.contains("invalid TOML"), "got: {msg}");
121 }
122
123 #[test]
124 fn platform_display() {
125 let inner = std::io::Error::other("dbus failure");
126 let err = Error::Platform(Box::new(inner));
127 let msg = err.to_string();
128 assert!(msg.contains("platform error"), "got: {msg}");
129 assert!(msg.contains("dbus failure"), "got: {msg}");
130 }
131
132 #[test]
133 fn platform_source_returns_inner() {
134 let inner = std::io::Error::other("inner error");
135 let err = Error::Platform(Box::new(inner));
136 let source = std::error::Error::source(&err);
137 assert!(source.is_some());
138 assert!(source.unwrap().to_string().contains("inner error"));
139 }
140
141 #[test]
142 fn non_platform_source_is_none() {
143 assert!(std::error::Error::source(&Error::Unsupported).is_none());
144 assert!(std::error::Error::source(&Error::Unavailable("x".into())).is_none());
145 assert!(std::error::Error::source(&Error::Format("x".into())).is_none());
146 }
147
148 #[test]
149 fn error_is_send_sync() {
150 fn assert_send_sync<T: Send + Sync>() {}
151 assert_send_sync::<Error>();
152 }
153
154 #[test]
155 fn from_toml_de_error() {
156 let toml_err: Result<toml::Value, toml::de::Error> = toml::from_str("=invalid");
158 let err: Error = toml_err.unwrap_err().into();
159 match &err {
160 Error::Format(msg) => assert!(!msg.is_empty()),
161 other => panic!("expected Format variant, got: {other:?}"),
162 }
163 }
164
165 #[test]
166 fn from_io_error() {
167 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing file");
168 let err: Error = io_err.into();
169 match &err {
170 Error::Io(e) => {
171 assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
172 assert!(e.to_string().contains("missing file"));
173 }
174 other => panic!("expected Io variant, got: {other:?}"),
175 }
176 }
177
178 #[test]
181 fn theme_resolution_error_construction() {
182 let e = ThemeResolutionError {
183 missing_fields: vec!["defaults.accent".into(), "button.font.family".into()],
184 };
185 assert_eq!(e.missing_fields.len(), 2);
186 assert_eq!(e.missing_fields[0], "defaults.accent");
187 assert_eq!(e.missing_fields[1], "button.font.family");
188 }
189
190 #[test]
191 fn theme_resolution_error_display_lists_count_and_fields() {
192 let e = ThemeResolutionError {
193 missing_fields: vec![
194 "defaults.accent".into(),
195 "button.foreground".into(),
196 "window.radius".into(),
197 ],
198 };
199 let msg = e.to_string();
200 assert!(msg.contains("3 missing field(s)"), "got: {msg}");
201 assert!(msg.contains("defaults.accent"), "got: {msg}");
202 assert!(msg.contains("button.foreground"), "got: {msg}");
203 assert!(msg.contains("window.radius"), "got: {msg}");
204 }
205
206 #[test]
207 fn theme_resolution_error_implements_std_error() {
208 let e = ThemeResolutionError {
209 missing_fields: vec!["defaults.accent".into()],
210 };
211 let _: &dyn std::error::Error = &e;
213 }
214
215 #[test]
216 fn theme_resolution_error_is_clone() {
217 let e = ThemeResolutionError {
218 missing_fields: vec!["defaults.accent".into()],
219 };
220 let e2 = e.clone();
221 assert_eq!(e.missing_fields, e2.missing_fields);
222 }
223
224 #[test]
227 fn error_resolution_variant_wraps_theme_resolution_error() {
228 let inner = ThemeResolutionError {
229 missing_fields: vec!["defaults.accent".into()],
230 };
231 let err = Error::Resolution(inner);
232 match &err {
233 Error::Resolution(e) => assert_eq!(e.missing_fields.len(), 1),
234 other => panic!("expected Resolution variant, got: {other:?}"),
235 }
236 }
237
238 #[test]
239 fn error_resolution_display_delegates_to_inner() {
240 let inner = ThemeResolutionError {
241 missing_fields: vec!["defaults.accent".into(), "window.radius".into()],
242 };
243 let err = Error::Resolution(inner);
244 let msg = err.to_string();
245 assert!(msg.contains("2 missing field(s)"), "got: {msg}");
246 assert!(msg.contains("defaults.accent"), "got: {msg}");
247 assert!(msg.contains("window.radius"), "got: {msg}");
248 }
249
250 #[test]
251 fn error_resolution_source_returns_inner() {
252 let inner = ThemeResolutionError {
253 missing_fields: vec!["defaults.accent".into()],
254 };
255 let err = Error::Resolution(inner);
256 let source = std::error::Error::source(&err);
257 assert!(
258 source.is_some(),
259 "source() should return Some for Resolution variant"
260 );
261 let source_msg = source.unwrap().to_string();
262 assert!(
263 source_msg.contains("1 missing field(s)"),
264 "got: {source_msg}"
265 );
266 }
267}