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 Resolution(ThemeResolutionError),
48}
49
50impl std::fmt::Display for Error {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Error::Unsupported => write!(f, "operation not supported on this platform"),
54 Error::Unavailable(msg) => write!(f, "theme data unavailable: {msg}"),
55 Error::Format(msg) => write!(f, "theme format error: {msg}"),
56 Error::Platform(err) => write!(f, "platform error: {err}"),
57 Error::Resolution(e) => write!(f, "{e}"),
58 }
59 }
60}
61
62impl std::error::Error for Error {
63 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
64 match self {
65 Error::Platform(err) => Some(&**err),
66 Error::Resolution(e) => Some(e),
67 _ => None,
68 }
69 }
70}
71
72impl From<toml::de::Error> for Error {
73 fn from(err: toml::de::Error) -> Self {
74 Error::Format(err.to_string())
75 }
76}
77
78impl From<toml::ser::Error> for Error {
79 fn from(err: toml::ser::Error) -> Self {
80 Error::Format(err.to_string())
81 }
82}
83
84impl From<std::io::Error> for Error {
85 fn from(err: std::io::Error) -> Self {
86 Error::Unavailable(err.to_string())
87 }
88}
89
90#[cfg(test)]
91#[allow(clippy::unwrap_used, clippy::expect_used)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn unsupported_display() {
97 let err = Error::Unsupported;
98 let msg = err.to_string();
99 assert!(msg.contains("not supported"), "got: {msg}");
100 }
101
102 #[test]
103 fn unavailable_display() {
104 let err = Error::Unavailable("file not found".into());
105 let msg = err.to_string();
106 assert!(msg.contains("unavailable"), "got: {msg}");
107 assert!(msg.contains("file not found"), "got: {msg}");
108 }
109
110 #[test]
111 fn format_display() {
112 let err = Error::Format("invalid TOML".into());
113 let msg = err.to_string();
114 assert!(msg.contains("format error"), "got: {msg}");
115 assert!(msg.contains("invalid TOML"), "got: {msg}");
116 }
117
118 #[test]
119 fn platform_display() {
120 let inner = std::io::Error::other("dbus failure");
121 let err = Error::Platform(Box::new(inner));
122 let msg = err.to_string();
123 assert!(msg.contains("platform error"), "got: {msg}");
124 assert!(msg.contains("dbus failure"), "got: {msg}");
125 }
126
127 #[test]
128 fn platform_source_returns_inner() {
129 let inner = std::io::Error::other("inner error");
130 let err = Error::Platform(Box::new(inner));
131 let source = std::error::Error::source(&err);
132 assert!(source.is_some());
133 assert!(source.unwrap().to_string().contains("inner error"));
134 }
135
136 #[test]
137 fn non_platform_source_is_none() {
138 assert!(std::error::Error::source(&Error::Unsupported).is_none());
139 assert!(std::error::Error::source(&Error::Unavailable("x".into())).is_none());
140 assert!(std::error::Error::source(&Error::Format("x".into())).is_none());
141 }
142
143 #[test]
144 fn error_is_send_sync() {
145 fn assert_send_sync<T: Send + Sync>() {}
146 assert_send_sync::<Error>();
147 }
148
149 #[test]
150 fn from_toml_de_error() {
151 let toml_err: Result<toml::Value, toml::de::Error> = toml::from_str("=invalid");
153 let err: Error = toml_err.unwrap_err().into();
154 match &err {
155 Error::Format(msg) => assert!(!msg.is_empty()),
156 other => panic!("expected Format variant, got: {other:?}"),
157 }
158 }
159
160 #[test]
161 fn from_io_error() {
162 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing file");
163 let err: Error = io_err.into();
164 match &err {
165 Error::Unavailable(msg) => assert!(msg.contains("missing file")),
166 other => panic!("expected Unavailable variant, got: {other:?}"),
167 }
168 }
169
170 #[test]
173 fn theme_resolution_error_construction() {
174 let e = ThemeResolutionError {
175 missing_fields: vec!["defaults.accent".into(), "button.font.family".into()],
176 };
177 assert_eq!(e.missing_fields.len(), 2);
178 assert_eq!(e.missing_fields[0], "defaults.accent");
179 assert_eq!(e.missing_fields[1], "button.font.family");
180 }
181
182 #[test]
183 fn theme_resolution_error_display_lists_count_and_fields() {
184 let e = ThemeResolutionError {
185 missing_fields: vec![
186 "defaults.accent".into(),
187 "button.foreground".into(),
188 "window.radius".into(),
189 ],
190 };
191 let msg = e.to_string();
192 assert!(msg.contains("3 missing field(s)"), "got: {msg}");
193 assert!(msg.contains("defaults.accent"), "got: {msg}");
194 assert!(msg.contains("button.foreground"), "got: {msg}");
195 assert!(msg.contains("window.radius"), "got: {msg}");
196 }
197
198 #[test]
199 fn theme_resolution_error_implements_std_error() {
200 let e = ThemeResolutionError {
201 missing_fields: vec!["defaults.accent".into()],
202 };
203 let _: &dyn std::error::Error = &e;
205 }
206
207 #[test]
208 fn theme_resolution_error_is_clone() {
209 let e = ThemeResolutionError {
210 missing_fields: vec!["defaults.accent".into()],
211 };
212 let e2 = e.clone();
213 assert_eq!(e.missing_fields, e2.missing_fields);
214 }
215
216 #[test]
219 fn error_resolution_variant_wraps_theme_resolution_error() {
220 let inner = ThemeResolutionError {
221 missing_fields: vec!["defaults.accent".into()],
222 };
223 let err = Error::Resolution(inner);
224 match &err {
225 Error::Resolution(e) => assert_eq!(e.missing_fields.len(), 1),
226 other => panic!("expected Resolution variant, got: {other:?}"),
227 }
228 }
229
230 #[test]
231 fn error_resolution_display_delegates_to_inner() {
232 let inner = ThemeResolutionError {
233 missing_fields: vec!["defaults.accent".into(), "window.radius".into()],
234 };
235 let err = Error::Resolution(inner);
236 let msg = err.to_string();
237 assert!(msg.contains("2 missing field(s)"), "got: {msg}");
238 assert!(msg.contains("defaults.accent"), "got: {msg}");
239 assert!(msg.contains("window.radius"), "got: {msg}");
240 }
241
242 #[test]
243 fn error_resolution_source_returns_inner() {
244 let inner = ThemeResolutionError {
245 missing_fields: vec!["defaults.accent".into()],
246 };
247 let err = Error::Resolution(inner);
248 let source = std::error::Error::source(&err);
249 assert!(
250 source.is_some(),
251 "source() should return Some for Resolution variant"
252 );
253 let source_msg = source.unwrap().to_string();
254 assert!(
255 source_msg.contains("1 missing field(s)"),
256 "got: {source_msg}"
257 );
258 }
259}