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}