use std::fmt;
use hwpforge_foundation::{
error::{ErrorCode, ErrorCodeExt, FoundationError},
HeadingType,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum BlueprintErrorCode {
YamlParse = 3000,
InvalidDimension = 3001,
CircularInheritance = 3002,
TemplateNotFound = 3003,
InheritanceDepthExceeded = 3004,
EmptyStyleMap = 3005,
StyleResolution = 3006,
DuplicateStyleName = 3007,
InvalidPercentage = 3008,
InvalidColor = 3009,
InvalidMappingReference = 3010,
InvalidStyleName = 3011,
InvalidTabReference = 3012,
DuplicateTabDefinition = 3013,
InvalidTabDefinition = 3014,
DuplicateNumberingDefinition = 3015,
DuplicateBulletDefinition = 3016,
InvalidListLevel = 3017,
InvalidListReference = 3018,
ConflictingListSpecification = 3019,
UnsupportedLegacyHeadingType = 3020,
InvalidCheckableBulletDefinition = 3021,
InvalidBulletDefinition = 3022,
InvalidPlainBulletDefinition = 3023,
}
impl fmt::Display for BlueprintErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "E{:04}", *self as u32)
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum BlueprintError {
#[error("YAML parse error: {message}")]
YamlParse {
message: String,
},
#[error("invalid dimension '{value}': expected format like '16pt', '20mm', or '1in'")]
InvalidDimension {
value: String,
},
#[error("invalid percentage '{value}': expected format like '160%'")]
InvalidPercentage {
value: String,
},
#[error("invalid color '{value}': expected '#RRGGBB' format")]
InvalidColor {
value: String,
},
#[error("circular template inheritance: {}", chain.join(" -> "))]
CircularInheritance {
chain: Vec<String>,
},
#[error("template not found: '{name}'")]
TemplateNotFound {
name: String,
},
#[error("inheritance depth {depth} exceeds maximum {max}")]
InheritanceDepthExceeded {
depth: usize,
max: usize,
},
#[error("template has no styles defined")]
EmptyStyleMap,
#[error("cannot resolve style '{style_name}': missing required field '{field}'")]
StyleResolution {
style_name: String,
field: String,
},
#[error("duplicate style name '{name}'")]
DuplicateStyleName {
name: String,
},
#[error("markdown mapping '{mapping_field}' references unknown style '{style_name}'")]
InvalidMappingReference {
mapping_field: String,
style_name: String,
},
#[error("invalid style name '{name}': {reason}")]
InvalidStyleName {
name: String,
reason: String,
},
#[error("style '{style_name}' references invalid tab definition {tab_id}: {reason}")]
InvalidTabReference {
style_name: String,
tab_id: u32,
reason: String,
},
#[error("duplicate tab definition id {id}")]
DuplicateTabDefinition {
id: u32,
},
#[error("tab definition {id} is invalid: {reason}")]
InvalidTabDefinition {
id: u32,
reason: String,
},
#[error("duplicate numbering definition id {id}")]
DuplicateNumberingDefinition {
id: u32,
},
#[error("duplicate bullet definition id {id}")]
DuplicateBulletDefinition {
id: u32,
},
#[error("style '{style_name}' uses invalid list level {level}: expected 0..={max}")]
InvalidListLevel {
style_name: String,
level: u8,
max: u8,
},
#[error("style '{style_name}' references unknown {kind} definition {id}")]
InvalidListReference {
style_name: String,
kind: String,
id: u32,
},
#[error("style '{style_name}' mixes legacy heading_type with explicit para_shape.list")]
ConflictingListSpecification {
style_name: String,
},
#[error(
"style '{style_name}' uses unsupported legacy heading_type '{heading_type:?}'; use para_shape.list instead"
)]
UnsupportedLegacyHeadingType {
style_name: String,
heading_type: HeadingType,
},
#[error("style '{style_name}' references non-checkable bullet definition {bullet_id}")]
InvalidCheckableBulletDefinition {
style_name: String,
bullet_id: u32,
},
#[error(
"style '{style_name}' references checkable bullet definition {bullet_id} as a plain bullet"
)]
InvalidPlainBulletDefinition {
style_name: String,
bullet_id: u32,
},
#[error("bullet definition {id} is invalid: {reason}")]
InvalidBulletDefinition {
id: u32,
reason: String,
},
#[error(transparent)]
Foundation(#[from] FoundationError),
}
impl ErrorCodeExt for BlueprintError {
fn code(&self) -> ErrorCode {
match self {
Self::Foundation(e) => e.code(),
Self::InvalidDimension { .. } | Self::InvalidPercentage { .. } => {
ErrorCode::InvalidField
}
Self::InvalidColor { .. } => ErrorCode::InvalidColor,
_ => ErrorCode::InvalidField,
}
}
}
impl BlueprintError {
pub fn blueprint_code(&self) -> BlueprintErrorCode {
match self {
Self::YamlParse { .. } => BlueprintErrorCode::YamlParse,
Self::InvalidDimension { .. } => BlueprintErrorCode::InvalidDimension,
Self::InvalidPercentage { .. } => BlueprintErrorCode::InvalidPercentage,
Self::InvalidColor { .. } => BlueprintErrorCode::InvalidColor,
Self::CircularInheritance { .. } => BlueprintErrorCode::CircularInheritance,
Self::TemplateNotFound { .. } => BlueprintErrorCode::TemplateNotFound,
Self::InheritanceDepthExceeded { .. } => BlueprintErrorCode::InheritanceDepthExceeded,
Self::EmptyStyleMap => BlueprintErrorCode::EmptyStyleMap,
Self::StyleResolution { .. } => BlueprintErrorCode::StyleResolution,
Self::DuplicateStyleName { .. } => BlueprintErrorCode::DuplicateStyleName,
Self::InvalidMappingReference { .. } => BlueprintErrorCode::InvalidMappingReference,
Self::InvalidStyleName { .. } => BlueprintErrorCode::InvalidStyleName,
Self::InvalidTabReference { .. } => BlueprintErrorCode::InvalidTabReference,
Self::DuplicateTabDefinition { .. } => BlueprintErrorCode::DuplicateTabDefinition,
Self::InvalidTabDefinition { .. } => BlueprintErrorCode::InvalidTabDefinition,
Self::DuplicateNumberingDefinition { .. } => {
BlueprintErrorCode::DuplicateNumberingDefinition
}
Self::DuplicateBulletDefinition { .. } => BlueprintErrorCode::DuplicateBulletDefinition,
Self::InvalidListLevel { .. } => BlueprintErrorCode::InvalidListLevel,
Self::InvalidListReference { .. } => BlueprintErrorCode::InvalidListReference,
Self::ConflictingListSpecification { .. } => {
BlueprintErrorCode::ConflictingListSpecification
}
Self::UnsupportedLegacyHeadingType { .. } => {
BlueprintErrorCode::UnsupportedLegacyHeadingType
}
Self::InvalidCheckableBulletDefinition { .. } => {
BlueprintErrorCode::InvalidCheckableBulletDefinition
}
Self::InvalidBulletDefinition { .. } => BlueprintErrorCode::InvalidBulletDefinition,
Self::InvalidPlainBulletDefinition { .. } => {
BlueprintErrorCode::InvalidPlainBulletDefinition
}
Self::Foundation(_) => BlueprintErrorCode::YamlParse, }
}
}
pub type BlueprintResult<T> = Result<T, BlueprintError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_code_display_format() {
assert_eq!(BlueprintErrorCode::YamlParse.to_string(), "E3000");
assert_eq!(BlueprintErrorCode::InvalidDimension.to_string(), "E3001");
assert_eq!(BlueprintErrorCode::CircularInheritance.to_string(), "E3002");
assert_eq!(BlueprintErrorCode::InvalidColor.to_string(), "E3009");
}
#[test]
fn error_code_range_is_3000() {
assert_eq!(BlueprintErrorCode::YamlParse as u32, 3000);
assert_eq!(BlueprintErrorCode::InvalidColor as u32, 3009);
}
#[test]
fn yaml_parse_error_message() {
let err = BlueprintError::YamlParse { message: "unexpected key 'foo'".into() };
assert_eq!(err.to_string(), "YAML parse error: unexpected key 'foo'");
assert_eq!(err.blueprint_code(), BlueprintErrorCode::YamlParse);
}
#[test]
fn invalid_dimension_error_message() {
let err = BlueprintError::InvalidDimension { value: "16px".into() };
assert!(err.to_string().contains("16px"));
assert!(err.to_string().contains("16pt"));
}
#[test]
fn circular_inheritance_shows_chain() {
let err =
BlueprintError::CircularInheritance { chain: vec!["a".into(), "b".into(), "a".into()] };
assert_eq!(err.to_string(), "circular template inheritance: a -> b -> a");
}
#[test]
fn template_not_found_message() {
let err = BlueprintError::TemplateNotFound { name: "missing_template".into() };
assert!(err.to_string().contains("missing_template"));
}
#[test]
fn inheritance_depth_exceeded_message() {
let err = BlueprintError::InheritanceDepthExceeded { depth: 15, max: 10 };
assert!(err.to_string().contains("15"));
assert!(err.to_string().contains("10"));
}
#[test]
fn style_resolution_error_message() {
let err =
BlueprintError::StyleResolution { style_name: "heading1".into(), field: "font".into() };
assert!(err.to_string().contains("heading1"));
assert!(err.to_string().contains("font"));
}
#[test]
fn foundation_error_propagation() {
let foundation_err = FoundationError::EmptyIdentifier { item: "FontId".into() };
let err: BlueprintError = foundation_err.into();
assert!(matches!(err, BlueprintError::Foundation(_)));
assert!(err.to_string().contains("FontId"));
}
#[test]
fn error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<BlueprintError>();
}
#[test]
fn error_implements_std_error() {
fn assert_error<T: std::error::Error>() {}
assert_error::<BlueprintError>();
}
#[test]
fn blueprint_code_mapping() {
let cases: Vec<(BlueprintError, BlueprintErrorCode)> = vec![
(BlueprintError::YamlParse { message: String::new() }, BlueprintErrorCode::YamlParse),
(
BlueprintError::InvalidDimension { value: String::new() },
BlueprintErrorCode::InvalidDimension,
),
(
BlueprintError::InvalidPercentage { value: String::new() },
BlueprintErrorCode::InvalidPercentage,
),
(
BlueprintError::InvalidColor { value: String::new() },
BlueprintErrorCode::InvalidColor,
),
(
BlueprintError::CircularInheritance { chain: vec![] },
BlueprintErrorCode::CircularInheritance,
),
(
BlueprintError::TemplateNotFound { name: String::new() },
BlueprintErrorCode::TemplateNotFound,
),
(
BlueprintError::InheritanceDepthExceeded { depth: 0, max: 0 },
BlueprintErrorCode::InheritanceDepthExceeded,
),
(BlueprintError::EmptyStyleMap, BlueprintErrorCode::EmptyStyleMap),
(
BlueprintError::StyleResolution { style_name: String::new(), field: String::new() },
BlueprintErrorCode::StyleResolution,
),
(
BlueprintError::DuplicateStyleName { name: String::new() },
BlueprintErrorCode::DuplicateStyleName,
),
(
BlueprintError::InvalidMappingReference {
mapping_field: String::new(),
style_name: String::new(),
},
BlueprintErrorCode::InvalidMappingReference,
),
(
BlueprintError::InvalidStyleName { name: String::new(), reason: String::new() },
BlueprintErrorCode::InvalidStyleName,
),
(
BlueprintError::InvalidBulletDefinition { id: 0, reason: String::new() },
BlueprintErrorCode::InvalidBulletDefinition,
),
(
BlueprintError::InvalidPlainBulletDefinition {
style_name: String::new(),
bullet_id: 0,
},
BlueprintErrorCode::InvalidPlainBulletDefinition,
),
];
for (err, expected_code) in cases {
assert_eq!(err.blueprint_code(), expected_code);
}
}
#[test]
fn error_code_ext_for_foundation_passthrough() {
let err = BlueprintError::Foundation(FoundationError::InvalidHwpUnit {
value: 999_999_999,
min: -100_000_000,
max: 100_000_000,
});
assert_eq!(err.code(), ErrorCode::InvalidHwpUnit);
}
}