1use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ErrorCode {
11 E001BlockNotFound,
13 E002InvalidBlockId,
14 E003InvalidDocumentId,
15
16 E100MalformedCommand,
18 E101InvalidPath,
19 E102InvalidValue,
20 E103UnexpectedToken,
21
22 E200SchemaViolation,
24 E201CycleDetected,
25 E202InvalidStructure,
26 E203OrphanedBlock,
27 E204DuplicateId,
28
29 E300VersionConflict,
31 E301TransactionTimeout,
32 E302DeadlockDetected,
33 E303TransactionNotFound,
34
35 E400DocumentSizeExceeded,
37 E401MemoryLimitExceeded,
38 E402BlockSizeExceeded,
39 E403NestingDepthExceeded,
40 E404EdgeCountExceeded,
41 E405ExecutionTimeout,
42
43 E500PathTraversal,
45 E501DisallowedScheme,
46 E502InvalidInput,
47
48 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 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 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#[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#[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 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 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 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 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 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#[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}