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)]
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}