Skip to main content

ucm_core/
error.rs

1//! Error types for UCM operations.
2
3use thiserror::Error;
4
5/// Result type alias using UCM Error
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Error codes for categorization and i18n
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ErrorCode {
11    // Reference errors (E001-E099)
12    E001BlockNotFound,
13    E002InvalidBlockId,
14    E003InvalidDocumentId,
15
16    // Syntax errors (E100-E199)
17    E100MalformedCommand,
18    E101InvalidPath,
19    E102InvalidValue,
20    E103UnexpectedToken,
21
22    // Validation errors (E200-E299)
23    E200SchemaViolation,
24    E201CycleDetected,
25    E202InvalidStructure,
26    E203OrphanedBlock,
27    E204DuplicateId,
28
29    // Concurrency errors (E300-E399)
30    E300VersionConflict,
31    E301TransactionTimeout,
32    E302DeadlockDetected,
33    E303TransactionNotFound,
34
35    // Resource errors (E400-E499)
36    E400DocumentSizeExceeded,
37    E401MemoryLimitExceeded,
38    E402BlockSizeExceeded,
39    E403NestingDepthExceeded,
40    E404EdgeCountExceeded,
41    E405ExecutionTimeout,
42
43    // Security errors (E500-E599)
44    E500PathTraversal,
45    E501DisallowedScheme,
46    E502InvalidInput,
47
48    // Internal errors (E900-E999)
49    E900InternalError,
50    E901SerializationError,
51    E902IoError,
52}
53
54impl std::fmt::Display for ErrorCode {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        write!(f, "{}", self.code())
57    }
58}
59
60impl ErrorCode {
61    /// Get the string code (e.g., "E001")
62    pub fn code(&self) -> &'static str {
63        match self {
64            Self::E001BlockNotFound => "E001",
65            Self::E002InvalidBlockId => "E002",
66            Self::E003InvalidDocumentId => "E003",
67            Self::E100MalformedCommand => "E100",
68            Self::E101InvalidPath => "E101",
69            Self::E102InvalidValue => "E102",
70            Self::E103UnexpectedToken => "E103",
71            Self::E200SchemaViolation => "E200",
72            Self::E201CycleDetected => "E201",
73            Self::E202InvalidStructure => "E202",
74            Self::E203OrphanedBlock => "E203",
75            Self::E204DuplicateId => "E204",
76            Self::E300VersionConflict => "E300",
77            Self::E301TransactionTimeout => "E301",
78            Self::E302DeadlockDetected => "E302",
79            Self::E303TransactionNotFound => "E303",
80            Self::E400DocumentSizeExceeded => "E400",
81            Self::E401MemoryLimitExceeded => "E401",
82            Self::E402BlockSizeExceeded => "E402",
83            Self::E403NestingDepthExceeded => "E403",
84            Self::E404EdgeCountExceeded => "E404",
85            Self::E405ExecutionTimeout => "E405",
86            Self::E500PathTraversal => "E500",
87            Self::E501DisallowedScheme => "E501",
88            Self::E502InvalidInput => "E502",
89            Self::E900InternalError => "E900",
90            Self::E901SerializationError => "E901",
91            Self::E902IoError => "E902",
92        }
93    }
94
95    /// Get a human-readable description
96    pub fn description(&self) -> &'static str {
97        match self {
98            Self::E001BlockNotFound => "Block does not exist",
99            Self::E002InvalidBlockId => "Invalid block ID format",
100            Self::E003InvalidDocumentId => "Invalid document ID format",
101            Self::E100MalformedCommand => "Malformed UCL command",
102            Self::E101InvalidPath => "Invalid path expression",
103            Self::E102InvalidValue => "Invalid value",
104            Self::E103UnexpectedToken => "Unexpected token",
105            Self::E200SchemaViolation => "Content schema violation",
106            Self::E201CycleDetected => "Cycle detected in structure",
107            Self::E202InvalidStructure => "Invalid document structure",
108            Self::E203OrphanedBlock => "Orphaned block detected",
109            Self::E204DuplicateId => "Duplicate block ID",
110            Self::E300VersionConflict => "Version conflict",
111            Self::E301TransactionTimeout => "Transaction timeout",
112            Self::E302DeadlockDetected => "Deadlock detected",
113            Self::E303TransactionNotFound => "Transaction not found",
114            Self::E400DocumentSizeExceeded => "Document size limit exceeded",
115            Self::E401MemoryLimitExceeded => "Memory limit exceeded",
116            Self::E402BlockSizeExceeded => "Block size limit exceeded",
117            Self::E403NestingDepthExceeded => "Nesting depth limit exceeded",
118            Self::E404EdgeCountExceeded => "Edge count limit exceeded",
119            Self::E405ExecutionTimeout => "Execution timeout",
120            Self::E500PathTraversal => "Path traversal attempt blocked",
121            Self::E501DisallowedScheme => "Disallowed URL scheme",
122            Self::E502InvalidInput => "Invalid input",
123            Self::E900InternalError => "Internal error",
124            Self::E901SerializationError => "Serialization error",
125            Self::E902IoError => "I/O error",
126        }
127    }
128}
129
130/// Location in source for error reporting
131#[derive(Debug, Clone, PartialEq, Eq, Default)]
132pub struct Location {
133    pub line: usize,
134    pub column: usize,
135    pub offset: usize,
136    pub length: usize,
137}
138
139impl Location {
140    pub fn new(line: usize, column: usize) -> Self {
141        Self {
142            line,
143            column,
144            offset: 0,
145            length: 0,
146        }
147    }
148
149    pub fn with_offset(mut self, offset: usize, length: usize) -> Self {
150        self.offset = offset;
151        self.length = length;
152        self
153    }
154}
155
156/// Main error type for UCM operations
157#[derive(Debug, Error)]
158pub enum Error {
159    #[error("[{code}] {message}")]
160    Ucm {
161        code: ErrorCode,
162        message: String,
163        location: Option<Location>,
164        context: Option<String>,
165        suggestion: Option<String>,
166    },
167
168    #[error("Block not found: {0}")]
169    BlockNotFound(String),
170
171    #[error("Invalid block ID: {0}")]
172    InvalidBlockId(String),
173
174    #[error("Invalid document ID: {0}")]
175    InvalidDocumentId(String),
176
177    #[error("Cycle detected at block: {0}")]
178    CycleDetected(String),
179
180    #[error("Version conflict: expected {expected}, found {actual}")]
181    VersionConflict { expected: u64, actual: u64 },
182
183    #[error("Validation error: {0}")]
184    Validation(String),
185
186    #[error("Parse error at line {line}, column {column}: {message}")]
187    Parse {
188        message: String,
189        line: usize,
190        column: usize,
191    },
192
193    #[error("Resource limit exceeded: {0}")]
194    ResourceLimit(String),
195
196    #[error("Security violation: {0}")]
197    Security(String),
198
199    #[error("Serialization error: {0}")]
200    Serialization(#[from] serde_json::Error),
201
202    #[error("I/O error: {0}")]
203    Io(#[from] std::io::Error),
204
205    #[error("Internal error: {0}")]
206    Internal(String),
207}
208
209impl Error {
210    /// Create a new UCM error with full details
211    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
212        Self::Ucm {
213            code,
214            message: message.into(),
215            location: None,
216            context: None,
217            suggestion: None,
218        }
219    }
220
221    /// Add location information
222    pub fn with_location(mut self, location: Location) -> Self {
223        if let Self::Ucm {
224            location: ref mut loc,
225            ..
226        } = self
227        {
228            *loc = Some(location);
229        }
230        self
231    }
232
233    /// Add context (e.g., the problematic command)
234    pub fn with_context(mut self, context: impl Into<String>) -> Self {
235        if let Self::Ucm {
236            context: ref mut ctx,
237            ..
238        } = self
239        {
240            *ctx = Some(context.into());
241        }
242        self
243    }
244
245    /// Add a suggestion for fixing the error
246    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
247        if let Self::Ucm {
248            suggestion: ref mut sug,
249            ..
250        } = self
251        {
252            *sug = Some(suggestion.into());
253        }
254        self
255    }
256
257    /// Get the error code if available
258    pub fn code(&self) -> Option<ErrorCode> {
259        match self {
260            Self::Ucm { code, .. } => Some(*code),
261            Self::BlockNotFound(_) => Some(ErrorCode::E001BlockNotFound),
262            Self::InvalidBlockId(_) => Some(ErrorCode::E002InvalidBlockId),
263            Self::InvalidDocumentId(_) => Some(ErrorCode::E003InvalidDocumentId),
264            Self::CycleDetected(_) => Some(ErrorCode::E201CycleDetected),
265            Self::VersionConflict { .. } => Some(ErrorCode::E300VersionConflict),
266            Self::Validation(_) => Some(ErrorCode::E200SchemaViolation),
267            Self::Parse { .. } => Some(ErrorCode::E100MalformedCommand),
268            Self::ResourceLimit(_) => Some(ErrorCode::E400DocumentSizeExceeded),
269            Self::Security(_) => Some(ErrorCode::E500PathTraversal),
270            Self::Serialization(_) => Some(ErrorCode::E901SerializationError),
271            Self::Io(_) => Some(ErrorCode::E902IoError),
272            Self::Internal(_) => Some(ErrorCode::E900InternalError),
273        }
274    }
275}
276
277/// Validation issue (warning or info level)
278#[derive(Debug, Clone)]
279pub struct ValidationIssue {
280    pub severity: ValidationSeverity,
281    pub code: ErrorCode,
282    pub message: String,
283    pub location: Option<Location>,
284    pub suggestion: Option<String>,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum ValidationSeverity {
289    Error,
290    Warning,
291    Info,
292}
293
294impl ValidationIssue {
295    pub fn error(code: ErrorCode, message: impl Into<String>) -> Self {
296        Self {
297            severity: ValidationSeverity::Error,
298            code,
299            message: message.into(),
300            location: None,
301            suggestion: None,
302        }
303    }
304
305    pub fn warning(code: ErrorCode, message: impl Into<String>) -> Self {
306        Self {
307            severity: ValidationSeverity::Warning,
308            code,
309            message: message.into(),
310            location: None,
311            suggestion: None,
312        }
313    }
314
315    pub fn info(code: ErrorCode, message: impl Into<String>) -> Self {
316        Self {
317            severity: ValidationSeverity::Info,
318            code,
319            message: message.into(),
320            location: None,
321            suggestion: None,
322        }
323    }
324
325    pub fn with_location(mut self, location: Location) -> Self {
326        self.location = Some(location);
327        self
328    }
329
330    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
331        self.suggestion = Some(suggestion.into());
332        self
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_error_code_strings() {
342        assert_eq!(ErrorCode::E001BlockNotFound.code(), "E001");
343        assert_eq!(
344            ErrorCode::E001BlockNotFound.description(),
345            "Block does not exist"
346        );
347    }
348
349    #[test]
350    fn test_error_with_details() {
351        let err = Error::new(ErrorCode::E001BlockNotFound, "Block 'blk_abc' not found")
352            .with_location(Location::new(10, 5))
353            .with_context("MOVE blk_abc TO blk_root")
354            .with_suggestion("Did you mean 'blk_abd'?");
355
356        assert_eq!(err.code(), Some(ErrorCode::E001BlockNotFound));
357    }
358}