Skip to main content

native_theme_build/
error.rs

1use std::fmt;
2
3use crate::schema::THEME_TABLE;
4
5/// Build-time error for icon code generation.
6///
7/// Each variant provides structured context for actionable error messages
8/// suitable for `cargo::error=` output.
9#[derive(Debug, Clone)]
10#[non_exhaustive]
11pub enum BuildError {
12    /// A role declared in the master TOML is missing from a theme mapping file.
13    MissingRole {
14        /// The role name that is missing.
15        role: String,
16        /// Path to the mapping file where the role is expected.
17        mapping_file: String,
18    },
19    /// An SVG file referenced by a bundled theme mapping does not exist.
20    MissingSvg {
21        /// Filesystem path to the missing SVG.
22        path: String,
23    },
24    /// A role in a mapping file is not declared in the master TOML.
25    UnknownRole {
26        /// The unexpected role name found in the mapping.
27        role: String,
28        /// Path to the mapping file containing the unknown role.
29        mapping_file: String,
30    },
31    /// A theme name does not match any known `IconSet` variant.
32    UnknownTheme {
33        /// The unrecognized theme name.
34        theme: String,
35    },
36    /// A DE-aware mapping value is missing the required `default` key.
37    MissingDefault {
38        /// The role whose DE-aware mapping lacks a default.
39        role: String,
40        /// Path to the mapping file.
41        mapping_file: String,
42    },
43    /// A role name appears in multiple TOML files.
44    DuplicateRole {
45        /// The duplicated role name.
46        role: String,
47        /// Path to the first file declaring the role.
48        file_a: String,
49        /// Path to the second file declaring the role.
50        file_b: String,
51    },
52    /// A TOML or SVG file could not be read or parsed.
53    Io {
54        /// Human-readable description of the I/O error.
55        message: String,
56    },
57    /// A role or enum name produces an invalid Rust identifier.
58    InvalidIdentifier {
59        /// The original name that failed validation.
60        name: String,
61        /// Why the identifier is invalid.
62        reason: String,
63    },
64    /// Two different role names produce the same PascalCase enum variant.
65    IdentifierCollision {
66        /// The first role name.
67        role_a: String,
68        /// The second role name.
69        role_b: String,
70        /// The PascalCase variant they both produce.
71        pascal: String,
72    },
73    /// A theme name appears in both `bundled_themes` and `system_themes`.
74    ThemeOverlap {
75        /// The overlapping theme name.
76        theme: String,
77    },
78    /// A role name appears more than once in a single config file.
79    DuplicateRoleInFile {
80        /// The duplicated role name.
81        role: String,
82        /// Path to the file containing the duplicate.
83        file: String,
84    },
85    /// A theme name appears more than once in the same list.
86    DuplicateTheme {
87        /// The duplicated theme name.
88        theme: String,
89        /// Which list contains the duplicate (`"bundled-themes"` or `"system-themes"`).
90        list: String,
91    },
92    /// A mapping value contains characters that are invalid for an icon name.
93    InvalidIconName {
94        /// The offending icon name.
95        name: String,
96        /// The role it belongs to.
97        role: String,
98        /// Path to the mapping file.
99        mapping_file: String,
100    },
101}
102
103impl fmt::Display for BuildError {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            Self::MissingRole { role, mapping_file } => {
107                write!(f, "role \"{role}\" is missing from {mapping_file}")
108            }
109            Self::MissingSvg { path } => {
110                write!(f, "SVG file not found: {path}")
111            }
112            Self::UnknownRole { role, mapping_file } => {
113                write!(
114                    f,
115                    "unknown role \"{role}\" in {mapping_file} (not declared in master TOML)"
116                )
117            }
118            Self::UnknownTheme { theme } => {
119                let expected: Vec<&str> = THEME_TABLE.iter().map(|(k, _)| *k).collect();
120                let list = expected.join(", ");
121                write!(f, "unknown theme \"{theme}\" (expected one of: {list})")
122            }
123            Self::MissingDefault { role, mapping_file } => {
124                write!(
125                    f,
126                    "DE-aware mapping for \"{role}\" in {mapping_file} is missing the required \"default\" key"
127                )
128            }
129            Self::DuplicateRole {
130                role,
131                file_a,
132                file_b,
133            } => {
134                write!(f, "role \"{role}\" defined in both {file_a} and {file_b}")
135            }
136            Self::Io { message } => {
137                write!(f, "{message}")
138            }
139            Self::InvalidIdentifier { name, reason } => {
140                write!(f, "invalid identifier \"{name}\": {reason}")
141            }
142            Self::IdentifierCollision {
143                role_a,
144                role_b,
145                pascal,
146            } => {
147                write!(
148                    f,
149                    "roles \"{role_a}\" and \"{role_b}\" both produce the same PascalCase variant \"{pascal}\""
150                )
151            }
152            Self::ThemeOverlap { theme } => {
153                write!(
154                    f,
155                    "theme \"{theme}\" appears in both bundled-themes and system-themes"
156                )
157            }
158            Self::DuplicateRoleInFile { role, file } => {
159                write!(f, "role \"{role}\" appears more than once in {file}")
160            }
161            Self::DuplicateTheme { theme, list } => {
162                write!(f, "theme \"{theme}\" appears more than once in {list}")
163            }
164            Self::InvalidIconName {
165                name,
166                role,
167                mapping_file,
168            } => {
169                write!(
170                    f,
171                    "invalid icon name \"{name}\" for role \"{role}\" in {mapping_file}: \
172                     names must be non-empty and free of control characters"
173                )
174            }
175        }
176    }
177}
178
179impl std::error::Error for BuildError {}
180
181/// A collection of [`BuildError`]s from a failed build pipeline.
182///
183/// Wraps a `Vec<BuildError>` and provides [`emit_cargo_errors()`](Self::emit_cargo_errors)
184/// for printing each error as a `cargo::error=` directive.
185#[derive(Debug, Clone)]
186pub struct BuildErrors(Vec<BuildError>);
187
188impl fmt::Display for BuildErrors {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        write!(f, "{} build error(s):", self.0.len())?;
191        for e in &self.0 {
192            write!(f, "\n  - {e}")?;
193        }
194        Ok(())
195    }
196}
197
198impl std::error::Error for BuildErrors {}
199
200impl BuildErrors {
201    /// Create a `BuildErrors` from a `Vec<BuildError>`.
202    pub(crate) fn new(errors: Vec<BuildError>) -> Self {
203        Self(errors)
204    }
205
206    /// Create a single-error `BuildErrors` from an I/O error message.
207    pub(crate) fn io(message: impl Into<String>) -> Self {
208        Self(vec![BuildError::Io {
209            message: message.into(),
210        }])
211    }
212
213    /// Return a borrowed slice of the contained errors.
214    pub fn errors(&self) -> &[BuildError] {
215        &self.0
216    }
217
218    /// Consume this collection and return the inner `Vec<BuildError>`.
219    pub fn into_errors(self) -> Vec<BuildError> {
220        self.0
221    }
222
223    /// Returns `true` if there are no errors.
224    pub fn is_empty(&self) -> bool {
225        self.0.is_empty()
226    }
227
228    /// Returns the number of errors.
229    pub fn len(&self) -> usize {
230        self.0.len()
231    }
232
233    /// Print each error as a `cargo::error=` directive to stdout.
234    pub fn emit_cargo_errors(&self) {
235        for e in &self.0 {
236            println!("cargo::error={e}");
237        }
238    }
239}
240
241impl IntoIterator for BuildErrors {
242    type Item = BuildError;
243    type IntoIter = std::vec::IntoIter<BuildError>;
244
245    fn into_iter(self) -> Self::IntoIter {
246        self.0.into_iter()
247    }
248}
249
250impl<'a> IntoIterator for &'a BuildErrors {
251    type Item = &'a BuildError;
252    type IntoIter = std::slice::Iter<'a, BuildError>;
253
254    fn into_iter(self) -> Self::IntoIter {
255        self.0.iter()
256    }
257}