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)]
10pub enum BuildError {
11 /// A role declared in the master TOML is missing from a theme mapping file.
12 MissingRole {
13 /// The role name that is missing.
14 role: String,
15 /// Path to the mapping file where the role is expected.
16 mapping_file: String,
17 },
18 /// An SVG file referenced by a bundled theme mapping does not exist.
19 MissingSvg {
20 /// Filesystem path to the missing SVG.
21 path: String,
22 },
23 /// A role in a mapping file is not declared in the master TOML.
24 UnknownRole {
25 /// The unexpected role name found in the mapping.
26 role: String,
27 /// Path to the mapping file containing the unknown role.
28 mapping_file: String,
29 },
30 /// A theme name does not match any known `IconSet` variant.
31 UnknownTheme {
32 /// The unrecognized theme name.
33 theme: String,
34 },
35 /// A DE-aware mapping value is missing the required `default` key.
36 MissingDefault {
37 /// The role whose DE-aware mapping lacks a default.
38 role: String,
39 /// Path to the mapping file.
40 mapping_file: String,
41 },
42 /// A role name appears in multiple TOML files.
43 DuplicateRole {
44 /// The duplicated role name.
45 role: String,
46 /// Path to the first file declaring the role.
47 file_a: String,
48 /// Path to the second file declaring the role.
49 file_b: String,
50 },
51 /// A TOML or SVG file could not be read or parsed.
52 Io {
53 /// Human-readable description of the I/O error.
54 message: String,
55 },
56 /// A role or enum name produces an invalid Rust identifier.
57 InvalidIdentifier {
58 /// The original name that failed validation.
59 name: String,
60 /// Why the identifier is invalid.
61 reason: String,
62 },
63 /// Two different role names produce the same PascalCase enum variant.
64 IdentifierCollision {
65 /// The first role name.
66 role_a: String,
67 /// The second role name.
68 role_b: String,
69 /// The PascalCase variant they both produce.
70 pascal: String,
71 },
72 /// A theme name appears in both `bundled_themes` and `system_themes`.
73 ThemeOverlap {
74 /// The overlapping theme name.
75 theme: String,
76 },
77 /// A role name appears more than once in a single config file.
78 DuplicateRoleInFile {
79 /// The duplicated role name.
80 role: String,
81 /// Path to the file containing the duplicate.
82 file: String,
83 },
84 /// A theme name appears more than once in the same list.
85 DuplicateTheme {
86 /// The duplicated theme name.
87 theme: String,
88 /// Which list contains the duplicate (`"bundled-themes"` or `"system-themes"`).
89 list: String,
90 },
91 /// A mapping value contains characters that are invalid for an icon name.
92 InvalidIconName {
93 /// The offending icon name.
94 name: String,
95 /// The role it belongs to.
96 role: String,
97 /// Path to the mapping file.
98 mapping_file: String,
99 },
100}
101
102impl fmt::Display for BuildError {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 Self::MissingRole { role, mapping_file } => {
106 write!(f, "role \"{role}\" is missing from {mapping_file}")
107 }
108 Self::MissingSvg { path } => {
109 write!(f, "SVG file not found: {path}")
110 }
111 Self::UnknownRole { role, mapping_file } => {
112 write!(
113 f,
114 "unknown role \"{role}\" in {mapping_file} (not declared in master TOML)"
115 )
116 }
117 Self::UnknownTheme { theme } => {
118 let expected: Vec<&str> = THEME_TABLE.iter().map(|(k, _)| *k).collect();
119 let list = expected.join(", ");
120 write!(f, "unknown theme \"{theme}\" (expected one of: {list})")
121 }
122 Self::MissingDefault { role, mapping_file } => {
123 write!(
124 f,
125 "DE-aware mapping for \"{role}\" in {mapping_file} is missing the required \"default\" key"
126 )
127 }
128 Self::DuplicateRole {
129 role,
130 file_a,
131 file_b,
132 } => {
133 write!(f, "role \"{role}\" defined in both {file_a} and {file_b}")
134 }
135 Self::Io { message } => {
136 write!(f, "{message}")
137 }
138 Self::InvalidIdentifier { name, reason } => {
139 write!(f, "invalid identifier \"{name}\": {reason}")
140 }
141 Self::IdentifierCollision {
142 role_a,
143 role_b,
144 pascal,
145 } => {
146 write!(
147 f,
148 "roles \"{role_a}\" and \"{role_b}\" both produce the same PascalCase variant \"{pascal}\""
149 )
150 }
151 Self::ThemeOverlap { theme } => {
152 write!(
153 f,
154 "theme \"{theme}\" appears in both bundled-themes and system-themes"
155 )
156 }
157 Self::DuplicateRoleInFile { role, file } => {
158 write!(f, "role \"{role}\" appears more than once in {file}")
159 }
160 Self::DuplicateTheme { theme, list } => {
161 write!(f, "theme \"{theme}\" appears more than once in {list}")
162 }
163 Self::InvalidIconName {
164 name,
165 role,
166 mapping_file,
167 } => {
168 write!(
169 f,
170 "invalid icon name \"{name}\" for role \"{role}\" in {mapping_file}: \
171 names must be non-empty and free of control characters"
172 )
173 }
174 }
175 }
176}
177
178impl std::error::Error for BuildError {}
179
180/// A collection of [`BuildError`]s from a failed build pipeline.
181///
182/// Wraps a `Vec<BuildError>` and provides [`emit_cargo_errors()`](Self::emit_cargo_errors)
183/// for printing each error as a `cargo::error=` directive.
184#[derive(Debug, Clone)]
185pub struct BuildErrors(pub Vec<BuildError>);
186
187impl fmt::Display for BuildErrors {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 write!(f, "{} build error(s):", self.0.len())?;
190 for e in &self.0 {
191 write!(f, "\n - {e}")?;
192 }
193 Ok(())
194 }
195}
196
197impl std::error::Error for BuildErrors {}
198
199impl BuildErrors {
200 /// Print each error as a `cargo::error=` directive to stdout.
201 pub fn emit_cargo_errors(&self) {
202 for e in &self.0 {
203 println!("cargo::error={e}");
204 }
205 }
206}