1use std::fmt;
16
17use hwpforge_foundation::error::{ErrorCode, ErrorCodeExt, FoundationError};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[repr(u32)]
22pub enum BlueprintErrorCode {
23 YamlParse = 3000,
25 InvalidDimension = 3001,
27 CircularInheritance = 3002,
29 TemplateNotFound = 3003,
31 InheritanceDepthExceeded = 3004,
33 EmptyStyleMap = 3005,
35 StyleResolution = 3006,
37 DuplicateStyleName = 3007,
39 InvalidPercentage = 3008,
41 InvalidColor = 3009,
43 InvalidMappingReference = 3010,
45 InvalidStyleName = 3011,
47}
48
49impl fmt::Display for BlueprintErrorCode {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 write!(f, "E{:04}", *self as u32)
52 }
53}
54
55#[derive(Debug, Clone, thiserror::Error)]
57#[non_exhaustive]
58pub enum BlueprintError {
59 #[error("YAML parse error: {message}")]
61 YamlParse {
62 message: String,
64 },
65
66 #[error("invalid dimension '{value}': expected format like '16pt', '20mm', or '1in'")]
68 InvalidDimension {
69 value: String,
71 },
72
73 #[error("invalid percentage '{value}': expected format like '160%'")]
75 InvalidPercentage {
76 value: String,
78 },
79
80 #[error("invalid color '{value}': expected '#RRGGBB' format")]
82 InvalidColor {
83 value: String,
85 },
86
87 #[error("circular template inheritance: {}", chain.join(" -> "))]
89 CircularInheritance {
90 chain: Vec<String>,
92 },
93
94 #[error("template not found: '{name}'")]
96 TemplateNotFound {
97 name: String,
99 },
100
101 #[error("inheritance depth {depth} exceeds maximum {max}")]
103 InheritanceDepthExceeded {
104 depth: usize,
106 max: usize,
108 },
109
110 #[error("template has no styles defined")]
112 EmptyStyleMap,
113
114 #[error("cannot resolve style '{style_name}': missing required field '{field}'")]
116 StyleResolution {
117 style_name: String,
119 field: String,
121 },
122
123 #[error("duplicate style name '{name}'")]
125 DuplicateStyleName {
126 name: String,
128 },
129
130 #[error("markdown mapping '{mapping_field}' references unknown style '{style_name}'")]
132 InvalidMappingReference {
133 mapping_field: String,
135 style_name: String,
137 },
138
139 #[error("invalid style name '{name}': {reason}")]
141 InvalidStyleName {
142 name: String,
144 reason: String,
146 },
147
148 #[error(transparent)]
150 Foundation(#[from] FoundationError),
151}
152
153impl ErrorCodeExt for BlueprintError {
154 fn code(&self) -> ErrorCode {
155 match self {
160 Self::Foundation(e) => e.code(),
161 Self::InvalidDimension { .. } | Self::InvalidPercentage { .. } => {
162 ErrorCode::InvalidField
163 }
164 Self::InvalidColor { .. } => ErrorCode::InvalidColor,
165 _ => ErrorCode::InvalidField,
166 }
167 }
168}
169
170impl BlueprintError {
171 pub fn blueprint_code(&self) -> BlueprintErrorCode {
173 match self {
174 Self::YamlParse { .. } => BlueprintErrorCode::YamlParse,
175 Self::InvalidDimension { .. } => BlueprintErrorCode::InvalidDimension,
176 Self::InvalidPercentage { .. } => BlueprintErrorCode::InvalidPercentage,
177 Self::InvalidColor { .. } => BlueprintErrorCode::InvalidColor,
178 Self::CircularInheritance { .. } => BlueprintErrorCode::CircularInheritance,
179 Self::TemplateNotFound { .. } => BlueprintErrorCode::TemplateNotFound,
180 Self::InheritanceDepthExceeded { .. } => BlueprintErrorCode::InheritanceDepthExceeded,
181 Self::EmptyStyleMap => BlueprintErrorCode::EmptyStyleMap,
182 Self::StyleResolution { .. } => BlueprintErrorCode::StyleResolution,
183 Self::DuplicateStyleName { .. } => BlueprintErrorCode::DuplicateStyleName,
184 Self::InvalidMappingReference { .. } => BlueprintErrorCode::InvalidMappingReference,
185 Self::InvalidStyleName { .. } => BlueprintErrorCode::InvalidStyleName,
186 Self::Foundation(_) => BlueprintErrorCode::YamlParse, }
188 }
189}
190
191pub type BlueprintResult<T> = Result<T, BlueprintError>;
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn error_code_display_format() {
200 assert_eq!(BlueprintErrorCode::YamlParse.to_string(), "E3000");
201 assert_eq!(BlueprintErrorCode::InvalidDimension.to_string(), "E3001");
202 assert_eq!(BlueprintErrorCode::CircularInheritance.to_string(), "E3002");
203 assert_eq!(BlueprintErrorCode::InvalidColor.to_string(), "E3009");
204 }
205
206 #[test]
207 fn error_code_range_is_3000() {
208 assert_eq!(BlueprintErrorCode::YamlParse as u32, 3000);
209 assert_eq!(BlueprintErrorCode::InvalidColor as u32, 3009);
210 }
211
212 #[test]
213 fn yaml_parse_error_message() {
214 let err = BlueprintError::YamlParse { message: "unexpected key 'foo'".into() };
215 assert_eq!(err.to_string(), "YAML parse error: unexpected key 'foo'");
216 assert_eq!(err.blueprint_code(), BlueprintErrorCode::YamlParse);
217 }
218
219 #[test]
220 fn invalid_dimension_error_message() {
221 let err = BlueprintError::InvalidDimension { value: "16px".into() };
222 assert!(err.to_string().contains("16px"));
223 assert!(err.to_string().contains("16pt"));
224 }
225
226 #[test]
227 fn circular_inheritance_shows_chain() {
228 let err =
229 BlueprintError::CircularInheritance { chain: vec!["a".into(), "b".into(), "a".into()] };
230 assert_eq!(err.to_string(), "circular template inheritance: a -> b -> a");
231 }
232
233 #[test]
234 fn template_not_found_message() {
235 let err = BlueprintError::TemplateNotFound { name: "missing_template".into() };
236 assert!(err.to_string().contains("missing_template"));
237 }
238
239 #[test]
240 fn inheritance_depth_exceeded_message() {
241 let err = BlueprintError::InheritanceDepthExceeded { depth: 15, max: 10 };
242 assert!(err.to_string().contains("15"));
243 assert!(err.to_string().contains("10"));
244 }
245
246 #[test]
247 fn style_resolution_error_message() {
248 let err =
249 BlueprintError::StyleResolution { style_name: "heading1".into(), field: "font".into() };
250 assert!(err.to_string().contains("heading1"));
251 assert!(err.to_string().contains("font"));
252 }
253
254 #[test]
255 fn foundation_error_propagation() {
256 let foundation_err = FoundationError::EmptyIdentifier { item: "FontId".into() };
257 let err: BlueprintError = foundation_err.into();
258 assert!(matches!(err, BlueprintError::Foundation(_)));
259 assert!(err.to_string().contains("FontId"));
260 }
261
262 #[test]
263 fn error_is_send_sync() {
264 fn assert_send_sync<T: Send + Sync>() {}
265 assert_send_sync::<BlueprintError>();
266 }
267
268 #[test]
269 fn error_implements_std_error() {
270 fn assert_error<T: std::error::Error>() {}
271 assert_error::<BlueprintError>();
272 }
273
274 #[test]
275 fn blueprint_code_mapping() {
276 let cases: Vec<(BlueprintError, BlueprintErrorCode)> = vec![
277 (BlueprintError::YamlParse { message: String::new() }, BlueprintErrorCode::YamlParse),
278 (
279 BlueprintError::InvalidDimension { value: String::new() },
280 BlueprintErrorCode::InvalidDimension,
281 ),
282 (
283 BlueprintError::InvalidPercentage { value: String::new() },
284 BlueprintErrorCode::InvalidPercentage,
285 ),
286 (
287 BlueprintError::InvalidColor { value: String::new() },
288 BlueprintErrorCode::InvalidColor,
289 ),
290 (
291 BlueprintError::CircularInheritance { chain: vec![] },
292 BlueprintErrorCode::CircularInheritance,
293 ),
294 (
295 BlueprintError::TemplateNotFound { name: String::new() },
296 BlueprintErrorCode::TemplateNotFound,
297 ),
298 (
299 BlueprintError::InheritanceDepthExceeded { depth: 0, max: 0 },
300 BlueprintErrorCode::InheritanceDepthExceeded,
301 ),
302 (BlueprintError::EmptyStyleMap, BlueprintErrorCode::EmptyStyleMap),
303 (
304 BlueprintError::StyleResolution { style_name: String::new(), field: String::new() },
305 BlueprintErrorCode::StyleResolution,
306 ),
307 (
308 BlueprintError::DuplicateStyleName { name: String::new() },
309 BlueprintErrorCode::DuplicateStyleName,
310 ),
311 (
312 BlueprintError::InvalidMappingReference {
313 mapping_field: String::new(),
314 style_name: String::new(),
315 },
316 BlueprintErrorCode::InvalidMappingReference,
317 ),
318 (
319 BlueprintError::InvalidStyleName { name: String::new(), reason: String::new() },
320 BlueprintErrorCode::InvalidStyleName,
321 ),
322 ];
323
324 for (err, expected_code) in cases {
325 assert_eq!(err.blueprint_code(), expected_code);
326 }
327 }
328
329 #[test]
330 fn error_code_ext_for_foundation_passthrough() {
331 let err = BlueprintError::Foundation(FoundationError::InvalidHwpUnit {
332 value: 999_999_999,
333 min: -100_000_000,
334 max: 100_000_000,
335 });
336 assert_eq!(err.code(), ErrorCode::InvalidHwpUnit);
337 }
338}