1use std::fmt;
2
3use crate::schema::THEME_TABLE;
4
5#[derive(Debug, Clone)]
10#[non_exhaustive]
11pub enum BuildError {
12 MissingRole {
14 role: String,
16 mapping_file: String,
18 },
19 MissingSvg {
21 path: String,
23 },
24 UnknownRole {
26 role: String,
28 mapping_file: String,
30 },
31 UnknownTheme {
33 theme: String,
35 source_file: Option<String>,
37 },
38 MissingDefault {
40 role: String,
42 mapping_file: String,
44 },
45 DuplicateRole {
47 role: String,
49 file_a: String,
51 file_b: String,
53 },
54 IoRead {
56 path: String,
58 reason: String,
60 },
61 IoParse {
63 path: String,
65 reason: String,
67 },
68 IoEnv {
70 var: String,
72 reason: String,
74 },
75 IoOther {
77 message: String,
79 },
80 InvalidIdentifier {
82 name: String,
84 reason: String,
86 },
87 IdentifierCollision {
89 role_a: String,
91 role_b: String,
93 pascal: String,
95 source_file: Option<String>,
97 },
98 ThemeOverlap {
100 theme: String,
102 },
103 DuplicateRoleInFile {
105 role: String,
107 file: String,
109 },
110 DuplicateTheme {
112 theme: String,
114 list: String,
116 },
117 InvalidIconName {
119 name: String,
121 role: String,
123 mapping_file: String,
125 offending: Option<char>,
127 },
128 BundledDeAware {
132 theme: String,
134 role: String,
136 },
137 InvalidCratePath {
139 path: String,
141 reason: String,
143 },
144 InvalidDerive {
146 name: String,
148 reason: String,
150 },
151}
152
153impl fmt::Display for BuildError {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 match self {
156 Self::MissingRole { role, mapping_file } => {
157 write!(f, "role \"{role}\" is missing from {mapping_file}")
158 }
159 Self::MissingSvg { path } => {
160 write!(f, "SVG file not found: {path}")
161 }
162 Self::UnknownRole { role, mapping_file } => {
163 write!(
164 f,
165 "unknown role \"{role}\" in {mapping_file} (not declared in master TOML)"
166 )
167 }
168 Self::UnknownTheme { theme, source_file } => {
169 let expected: Vec<&str> = THEME_TABLE.iter().map(|(k, _)| *k).collect();
170 let list = expected.join(", ");
171 write!(f, "unknown theme \"{theme}\" (expected one of: {list})")?;
172 if let Some(file) = source_file {
173 write!(f, " in {file}")?;
174 }
175 Ok(())
176 }
177 Self::MissingDefault { role, mapping_file } => {
178 write!(
179 f,
180 "DE-aware mapping for \"{role}\" in {mapping_file} is missing the required \"default\" key"
181 )
182 }
183 Self::DuplicateRole {
184 role,
185 file_a,
186 file_b,
187 } => {
188 write!(f, "role \"{role}\" defined in both {file_a} and {file_b}")
189 }
190 Self::IoRead { path, reason } => {
191 write!(f, "failed to read {path}: {reason}")
192 }
193 Self::IoParse { path, reason } => {
194 write!(f, "failed to parse {path}: {reason}")
195 }
196 Self::IoEnv { var, reason } => {
197 write!(f, "environment variable {var} not available: {reason}")
198 }
199 Self::IoOther { message } => {
200 write!(f, "{message}")
201 }
202 Self::InvalidIdentifier { name, reason } => {
203 write!(f, "invalid identifier \"{name}\": {reason}")
204 }
205 Self::IdentifierCollision {
206 role_a,
207 role_b,
208 pascal,
209 source_file,
210 } => {
211 write!(
212 f,
213 "roles \"{role_a}\" and \"{role_b}\" both produce the same PascalCase variant \"{pascal}\""
214 )?;
215 if let Some(file) = source_file {
216 write!(f, " (in {file})")?;
217 }
218 Ok(())
219 }
220 Self::ThemeOverlap { theme } => {
221 write!(
222 f,
223 "theme \"{theme}\" appears in both bundled-themes and system-themes"
224 )
225 }
226 Self::DuplicateRoleInFile { role, file } => {
227 write!(f, "role \"{role}\" appears more than once in {file}")
228 }
229 Self::DuplicateTheme { theme, list } => {
230 write!(f, "theme \"{theme}\" appears more than once in {list}")
231 }
232 Self::InvalidIconName {
233 name,
234 role,
235 mapping_file,
236 offending,
237 } => {
238 write!(
239 f,
240 "invalid icon name \"{name}\" for role \"{role}\" in {mapping_file}"
241 )?;
242 if let Some(ch) = offending {
243 write!(f, " (contains '\\u{{{:04X}}}')", *ch as u32)?;
244 }
245 write!(
246 f,
247 ": names must be non-empty and free of control characters"
248 )
249 }
250 Self::BundledDeAware { theme, role } => {
251 write!(
252 f,
253 "bundled theme \"{theme}\" has DE-aware mapping for role \"{role}\": \
254 bundled themes can only embed one SVG per role, but DE-aware mappings \
255 declare multiple icon names. Use a system theme for DE-aware icons"
256 )
257 }
258 Self::InvalidCratePath { path, reason } => {
259 write!(f, "invalid crate_path \"{path}\": {reason}")
260 }
261 Self::InvalidDerive { name, reason } => {
262 write!(f, "invalid derive \"{name}\": {reason}")
263 }
264 }
265 }
266}
267
268impl std::error::Error for BuildError {}
269
270#[derive(Debug, Clone)]
277pub struct BuildErrors {
278 errors: Vec<BuildError>,
279 pub rerun_paths: Vec<std::path::PathBuf>,
281}
282
283impl fmt::Display for BuildErrors {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 write!(f, "{} build error(s):", self.errors.len())?;
286 for e in &self.errors {
287 write!(f, "\n - {e}")?;
288 }
289 Ok(())
290 }
291}
292
293impl std::error::Error for BuildErrors {}
294
295impl BuildErrors {
296 pub(crate) fn new(errors: Vec<BuildError>) -> Self {
298 debug_assert!(
299 !errors.is_empty(),
300 "BuildErrors created with empty error list"
301 );
302 Self {
303 errors,
304 rerun_paths: Vec::new(),
305 }
306 }
307
308 pub(crate) fn with_rerun_paths(
310 errors: Vec<BuildError>,
311 rerun_paths: Vec<std::path::PathBuf>,
312 ) -> Self {
313 Self {
314 errors,
315 rerun_paths,
316 }
317 }
318
319 pub(crate) fn io_read(path: impl Into<String>, reason: impl Into<String>) -> Self {
321 Self {
322 errors: vec![BuildError::IoRead {
323 path: path.into(),
324 reason: reason.into(),
325 }],
326 rerun_paths: Vec::new(),
327 }
328 }
329
330 pub(crate) fn io_parse(path: impl Into<String>, reason: impl Into<String>) -> Self {
332 Self {
333 errors: vec![BuildError::IoParse {
334 path: path.into(),
335 reason: reason.into(),
336 }],
337 rerun_paths: Vec::new(),
338 }
339 }
340
341 pub(crate) fn io_env(var: impl Into<String>, reason: impl Into<String>) -> Self {
343 Self {
344 errors: vec![BuildError::IoEnv {
345 var: var.into(),
346 reason: reason.into(),
347 }],
348 rerun_paths: Vec::new(),
349 }
350 }
351
352 pub(crate) fn io_other(message: impl Into<String>) -> Self {
355 Self {
356 errors: vec![BuildError::IoOther {
357 message: message.into(),
358 }],
359 rerun_paths: Vec::new(),
360 }
361 }
362
363 pub fn errors(&self) -> &[BuildError] {
365 &self.errors
366 }
367
368 pub fn into_errors(self) -> Vec<BuildError> {
370 self.errors
371 }
372
373 pub fn is_empty(&self) -> bool {
375 self.errors.is_empty()
376 }
377
378 pub fn len(&self) -> usize {
380 self.errors.len()
381 }
382
383 pub fn emit_cargo_errors(&self) {
388 for path in &self.rerun_paths {
389 println!("cargo::rerun-if-changed={}", path.display());
390 }
391 for e in &self.errors {
392 println!("cargo::error={e}");
393 }
394 }
395}
396
397impl IntoIterator for BuildErrors {
398 type Item = BuildError;
399 type IntoIter = std::vec::IntoIter<BuildError>;
400
401 fn into_iter(self) -> Self::IntoIter {
402 self.errors.into_iter()
403 }
404}
405
406impl<'a> IntoIterator for &'a BuildErrors {
407 type Item = &'a BuildError;
408 type IntoIter = std::slice::Iter<'a, BuildError>;
409
410 fn into_iter(self) -> Self::IntoIter {
411 self.errors.iter()
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn build_errors_is_empty_and_len() {
421 let errors = BuildErrors::new(vec![BuildError::IoOther {
422 message: "test".into(),
423 }]);
424 assert!(!errors.is_empty());
425 assert_eq!(errors.len(), 1);
426 }
427
428 #[test]
429 fn build_errors_len_multiple() {
430 let errors = BuildErrors::new(vec![
431 BuildError::IoRead {
432 path: "file.toml".into(),
433 reason: "first".into(),
434 },
435 BuildError::IoParse {
436 path: "file.toml".into(),
437 reason: "second".into(),
438 },
439 ]);
440 assert!(!errors.is_empty());
441 assert_eq!(errors.len(), 2);
442 }
443
444 #[test]
445 fn build_errors_display_shows_count() {
446 let errors = BuildErrors::new(vec![BuildError::IoOther {
447 message: "oops".into(),
448 }]);
449 let display = format!("{errors}");
450 assert!(display.contains("1 build error(s)"));
451 assert!(display.contains("oops"));
452 }
453
454 #[test]
455 fn build_errors_into_iter() {
456 let errors = BuildErrors::new(vec![BuildError::IoOther {
457 message: "iter".into(),
458 }]);
459 let collected: Vec<BuildError> = errors.into_iter().collect();
460 assert_eq!(collected.len(), 1);
461 }
462
463 #[test]
464 fn build_errors_ref_iter() {
465 let errors = BuildErrors::new(vec![BuildError::IoOther {
466 message: "ref".into(),
467 }]);
468 let collected: Vec<&BuildError> = (&errors).into_iter().collect();
469 assert_eq!(collected.len(), 1);
470 }
471}