hwpforge_smithy_md/
error.rs

1//! Error types for the Markdown Smithy.
2
3use std::fmt;
4
5/// Top-level error type for smithy-md operations.
6#[derive(Debug, thiserror::Error)]
7#[non_exhaustive]
8pub enum MdError {
9    /// YAML frontmatter exists but failed to parse.
10    #[error("invalid YAML frontmatter: {detail}")]
11    InvalidFrontmatter {
12        /// Parsing error details.
13        detail: String,
14    },
15
16    /// The document started a frontmatter block but never closed it.
17    #[error("frontmatter block started with '---' but no closing marker was found")]
18    FrontmatterUnclosed,
19
20    /// Template inheritance could not be resolved with available providers.
21    #[error("template resolution failed: {detail}")]
22    TemplateResolution {
23        /// Resolution error details.
24        detail: String,
25    },
26
27    /// The markdown content contains a structure this decoder cannot map.
28    #[error("unsupported markdown structure: {detail}")]
29    UnsupportedStructure {
30        /// Unsupported structure details.
31        detail: String,
32    },
33
34    /// Lossless body parsing failed.
35    #[error("invalid lossless body: {detail}")]
36    LosslessParse {
37        /// Parsing error details.
38        detail: String,
39    },
40
41    /// Required attribute is missing in a lossless element.
42    #[error("missing required attribute '{attribute}' on <{element}>")]
43    LosslessMissingAttribute {
44        /// Element name.
45        element: &'static str,
46        /// Missing attribute name.
47        attribute: &'static str,
48    },
49
50    /// Attribute value in a lossless element is invalid.
51    #[error("invalid attribute '{attribute}' on <{element}>: {value}")]
52    LosslessInvalidAttribute {
53        /// Element name.
54        element: &'static str,
55        /// Attribute name.
56        attribute: &'static str,
57        /// Invalid value.
58        value: String,
59    },
60
61    /// Input file exceeds the maximum allowed size.
62    #[error("file too large: {size} bytes exceeds {limit} byte limit")]
63    FileTooLarge {
64        /// Actual file size in bytes.
65        size: u64,
66        /// Maximum allowed size in bytes.
67        limit: u64,
68    },
69
70    /// I/O error for file convenience APIs.
71    #[error("I/O error: {0}")]
72    Io(#[from] std::io::Error),
73
74    /// Core-layer error propagated upward.
75    #[error("core error: {0}")]
76    Core(#[from] hwpforge_core::CoreError),
77
78    /// Blueprint-layer error propagated upward.
79    #[error("blueprint error: {0}")]
80    Blueprint(#[from] hwpforge_blueprint::error::BlueprintError),
81
82    /// Foundation-layer error propagated upward.
83    #[error("foundation error: {0}")]
84    Foundation(#[from] hwpforge_foundation::FoundationError),
85}
86
87/// Error codes for smithy-md (6000-6999 range).
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89#[non_exhaustive]
90pub enum MdErrorCode {
91    /// Invalid YAML frontmatter.
92    InvalidFrontmatter = 6000,
93    /// Frontmatter delimiter was not closed.
94    FrontmatterUnclosed = 6001,
95    /// Template inheritance resolution failed.
96    TemplateResolution = 6002,
97    /// Unsupported markdown structure.
98    UnsupportedStructure = 6003,
99    /// Invalid lossless body.
100    LosslessParse = 6008,
101    /// Missing lossless element attribute.
102    LosslessMissingAttribute = 6009,
103    /// Invalid lossless element attribute value.
104    LosslessInvalidAttribute = 6010,
105    /// File exceeds maximum allowed size.
106    FileTooLarge = 6011,
107    /// I/O failure.
108    Io = 6004,
109    /// Propagated Core error.
110    Core = 6005,
111    /// Propagated Blueprint error.
112    Blueprint = 6006,
113    /// Propagated Foundation error.
114    Foundation = 6007,
115}
116
117impl fmt::Display for MdErrorCode {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(f, "E{}", *self as u16)
120    }
121}
122
123impl MdError {
124    /// Returns the corresponding stable error code.
125    pub fn code(&self) -> MdErrorCode {
126        match self {
127            Self::InvalidFrontmatter { .. } => MdErrorCode::InvalidFrontmatter,
128            Self::FrontmatterUnclosed => MdErrorCode::FrontmatterUnclosed,
129            Self::TemplateResolution { .. } => MdErrorCode::TemplateResolution,
130            Self::UnsupportedStructure { .. } => MdErrorCode::UnsupportedStructure,
131            Self::LosslessParse { .. } => MdErrorCode::LosslessParse,
132            Self::LosslessMissingAttribute { .. } => MdErrorCode::LosslessMissingAttribute,
133            Self::LosslessInvalidAttribute { .. } => MdErrorCode::LosslessInvalidAttribute,
134            Self::FileTooLarge { .. } => MdErrorCode::FileTooLarge,
135            Self::Io(_) => MdErrorCode::Io,
136            Self::Core(_) => MdErrorCode::Core,
137            Self::Blueprint(_) => MdErrorCode::Blueprint,
138            Self::Foundation(_) => MdErrorCode::Foundation,
139        }
140    }
141}
142
143/// Result alias used throughout smithy-md.
144pub type MdResult<T> = Result<T, MdError>;
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn code_display_format() {
152        assert_eq!(MdErrorCode::InvalidFrontmatter.to_string(), "E6000");
153        assert_eq!(MdErrorCode::LosslessParse.to_string(), "E6008");
154        assert_eq!(MdErrorCode::Foundation.to_string(), "E6007");
155    }
156
157    #[test]
158    fn code_mapping_for_frontmatter() {
159        let err = MdError::FrontmatterUnclosed;
160        assert_eq!(err.code(), MdErrorCode::FrontmatterUnclosed);
161    }
162
163    #[test]
164    fn unsupported_structure_variant_has_code() {
165        let err = MdError::UnsupportedStructure { detail: "definition list".to_string() };
166        assert_eq!(err.code(), MdErrorCode::UnsupportedStructure);
167    }
168
169    #[test]
170    fn lossless_attribute_error_code_mapping() {
171        let err = MdError::LosslessMissingAttribute { element: "img", attribute: "src" };
172        assert_eq!(err.code(), MdErrorCode::LosslessMissingAttribute);
173    }
174
175    #[test]
176    fn file_too_large_error_code_and_display() {
177        let err = MdError::FileTooLarge { size: 100_000_000, limit: 50_000_000 };
178        assert_eq!(err.code(), MdErrorCode::FileTooLarge);
179        assert!(err.to_string().contains("100000000"));
180        assert!(err.to_string().contains("50000000"));
181    }
182}