1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[repr(u32)]
13pub enum ErrorCode {
14 ParseError = 1,
16 NetworkError = 2,
18 AuthError = 3,
20 NotFound = 4,
22 ServerError = 5,
24 Timeout = 6,
26 InvalidInput = 7,
28 Unsupported = 8,
30}
31
32impl ErrorCode {
33 pub fn from_u32(n: u32) -> Option<Self> {
35 match n {
36 1 => Some(Self::ParseError),
37 2 => Some(Self::NetworkError),
38 3 => Some(Self::AuthError),
39 4 => Some(Self::NotFound),
40 5 => Some(Self::ServerError),
41 6 => Some(Self::Timeout),
42 7 => Some(Self::InvalidInput),
43 8 => Some(Self::Unsupported),
44 _ => None,
45 }
46 }
47
48 pub fn as_u32(self) -> u32 {
50 self as u32
51 }
52}
53
54impl std::fmt::Display for ErrorCode {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 let name = match self {
57 Self::ParseError => "ParseError",
58 Self::NetworkError => "NetworkError",
59 Self::AuthError => "AuthError",
60 Self::NotFound => "NotFound",
61 Self::ServerError => "ServerError",
62 Self::Timeout => "Timeout",
63 Self::InvalidInput => "InvalidInput",
64 Self::Unsupported => "Unsupported",
65 };
66 write!(f, "{name}")
67 }
68}
69
70#[derive(Debug, Clone)]
76pub struct WasmError {
77 pub code: ErrorCode,
79 pub message: String,
81 pub details: Option<String>,
83 pub recoverable: bool,
85}
86
87impl WasmError {
88 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
90 Self {
91 code,
92 message: message.into(),
93 details: None,
94 recoverable: true,
95 }
96 }
97
98 pub fn with_details(mut self, details: impl Into<String>) -> Self {
100 self.details = Some(details.into());
101 self
102 }
103
104 pub fn unrecoverable(mut self) -> Self {
106 self.recoverable = false;
107 self
108 }
109
110 pub fn to_json(&self) -> String {
112 let details_part = match &self.details {
113 Some(d) => format!(r#","details":"{}""#, escape_json(d)),
114 None => String::new(),
115 };
116 format!(
117 r#"{{"code":{},"name":"{}","message":"{}","recoverable":{}{}}}"#,
118 self.code.as_u32(),
119 self.code,
120 escape_json(&self.message),
121 self.recoverable,
122 details_part,
123 )
124 }
125
126 pub fn from_code(code: u32, message: &str) -> Option<Self> {
130 ErrorCode::from_u32(code).map(|ec| Self::new(ec, message))
131 }
132
133 pub fn is_client_error(&self) -> bool {
135 matches!(
136 self.code,
137 ErrorCode::ParseError
138 | ErrorCode::NotFound
139 | ErrorCode::InvalidInput
140 | ErrorCode::Unsupported
141 )
142 }
143
144 pub fn is_server_error(&self) -> bool {
146 matches!(
147 self.code,
148 ErrorCode::NetworkError | ErrorCode::ServerError | ErrorCode::Timeout
149 )
150 }
151}
152
153impl std::fmt::Display for WasmError {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 write!(f, "[{}] {}", self.code, self.message)
156 }
157}
158
159impl std::error::Error for WasmError {}
160
161pub struct ErrorHandler {
167 history: Vec<WasmError>,
168 max_history: usize,
169}
170
171impl ErrorHandler {
172 pub fn new(max_history: usize) -> Self {
174 Self {
175 history: Vec::new(),
176 max_history: max_history.max(1),
177 }
178 }
179
180 pub fn handle(&mut self, error: WasmError) -> String {
183 let json = error.to_json();
184 if self.history.len() >= self.max_history {
185 self.history.remove(0);
186 }
187 self.history.push(error);
188 json
189 }
190
191 pub fn last_error(&self) -> Option<&WasmError> {
193 self.history.last()
194 }
195
196 pub fn error_count(&self) -> usize {
198 self.history.len()
199 }
200
201 pub fn has_unrecoverable(&self) -> bool {
203 self.history.iter().any(|e| !e.recoverable)
204 }
205
206 pub fn clear_history(&mut self) {
208 self.history.clear();
209 }
210
211 pub fn errors_by_code(&self, code: u32) -> Vec<&WasmError> {
213 self.history
214 .iter()
215 .filter(|e| e.code.as_u32() == code)
216 .collect()
217 }
218}
219
220fn escape_json(s: &str) -> String {
225 s.chars()
226 .flat_map(|c| match c {
227 '"' => vec!['\\', '"'],
228 '\\' => vec!['\\', '\\'],
229 '\n' => vec!['\\', 'n'],
230 '\r' => vec!['\\', 'r'],
231 '\t' => vec!['\\', 't'],
232 other => vec![other],
233 })
234 .collect()
235}
236
237#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
249 fn test_error_code_as_u32() {
250 assert_eq!(ErrorCode::ParseError.as_u32(), 1);
251 assert_eq!(ErrorCode::NetworkError.as_u32(), 2);
252 assert_eq!(ErrorCode::AuthError.as_u32(), 3);
253 assert_eq!(ErrorCode::NotFound.as_u32(), 4);
254 assert_eq!(ErrorCode::ServerError.as_u32(), 5);
255 assert_eq!(ErrorCode::Timeout.as_u32(), 6);
256 assert_eq!(ErrorCode::InvalidInput.as_u32(), 7);
257 assert_eq!(ErrorCode::Unsupported.as_u32(), 8);
258 }
259
260 #[test]
261 fn test_error_code_from_u32_valid() {
262 assert_eq!(ErrorCode::from_u32(1), Some(ErrorCode::ParseError));
263 assert_eq!(ErrorCode::from_u32(8), Some(ErrorCode::Unsupported));
264 }
265
266 #[test]
267 fn test_error_code_from_u32_invalid() {
268 assert_eq!(ErrorCode::from_u32(0), None);
269 assert_eq!(ErrorCode::from_u32(9), None);
270 assert_eq!(ErrorCode::from_u32(u32::MAX), None);
271 }
272
273 #[test]
274 fn test_error_code_display() {
275 assert_eq!(ErrorCode::ParseError.to_string(), "ParseError");
276 assert_eq!(ErrorCode::Unsupported.to_string(), "Unsupported");
277 }
278
279 #[test]
280 fn test_error_code_equality() {
281 assert_eq!(ErrorCode::AuthError, ErrorCode::AuthError);
282 assert_ne!(ErrorCode::AuthError, ErrorCode::NotFound);
283 }
284
285 #[test]
286 fn test_error_code_all_round_trip() {
287 for n in 1u32..=8 {
288 let code = ErrorCode::from_u32(n).expect("valid code");
289 assert_eq!(code.as_u32(), n);
290 }
291 }
292
293 #[test]
297 fn test_wasm_error_new_code() {
298 let e = WasmError::new(ErrorCode::ParseError, "bad syntax");
299 assert_eq!(e.code, ErrorCode::ParseError);
300 }
301
302 #[test]
303 fn test_wasm_error_new_message() {
304 let e = WasmError::new(ErrorCode::NotFound, "not found");
305 assert_eq!(e.message, "not found");
306 }
307
308 #[test]
309 fn test_wasm_error_new_recoverable_default() {
310 let e = WasmError::new(ErrorCode::ParseError, "x");
311 assert!(e.recoverable);
312 }
313
314 #[test]
315 fn test_wasm_error_new_no_details() {
316 let e = WasmError::new(ErrorCode::ParseError, "x");
317 assert!(e.details.is_none());
318 }
319
320 #[test]
324 fn test_with_details() {
325 let e = WasmError::new(ErrorCode::ServerError, "msg").with_details("inner error");
326 assert_eq!(e.details.as_deref(), Some("inner error"));
327 }
328
329 #[test]
330 fn test_unrecoverable() {
331 let e = WasmError::new(ErrorCode::ServerError, "fatal").unrecoverable();
332 assert!(!e.recoverable);
333 }
334
335 #[test]
336 fn test_builder_chaining() {
337 let e = WasmError::new(ErrorCode::Timeout, "timed out")
338 .with_details("after 30s")
339 .unrecoverable();
340 assert!(!e.recoverable);
341 assert_eq!(e.details.as_deref(), Some("after 30s"));
342 }
343
344 #[test]
348 fn test_from_code_valid() {
349 let e = WasmError::from_code(2, "network failure");
350 assert!(e.is_some());
351 let e = e.expect("should succeed");
352 assert_eq!(e.code, ErrorCode::NetworkError);
353 }
354
355 #[test]
356 fn test_from_code_invalid() {
357 assert!(WasmError::from_code(99, "bad").is_none());
358 }
359
360 #[test]
361 fn test_from_code_zero() {
362 assert!(WasmError::from_code(0, "zero").is_none());
363 }
364
365 #[test]
369 fn test_is_client_error_parse() {
370 let e = WasmError::new(ErrorCode::ParseError, "x");
371 assert!(e.is_client_error());
372 assert!(!e.is_server_error());
373 }
374
375 #[test]
376 fn test_is_client_error_not_found() {
377 assert!(WasmError::new(ErrorCode::NotFound, "x").is_client_error());
378 }
379
380 #[test]
381 fn test_is_client_error_invalid_input() {
382 assert!(WasmError::new(ErrorCode::InvalidInput, "x").is_client_error());
383 }
384
385 #[test]
386 fn test_is_client_error_unsupported() {
387 assert!(WasmError::new(ErrorCode::Unsupported, "x").is_client_error());
388 }
389
390 #[test]
391 fn test_is_server_error_network() {
392 let e = WasmError::new(ErrorCode::NetworkError, "x");
393 assert!(e.is_server_error());
394 assert!(!e.is_client_error());
395 }
396
397 #[test]
398 fn test_is_server_error_internal() {
399 assert!(WasmError::new(ErrorCode::ServerError, "x").is_server_error());
400 }
401
402 #[test]
403 fn test_is_server_error_timeout() {
404 assert!(WasmError::new(ErrorCode::Timeout, "x").is_server_error());
405 }
406
407 #[test]
408 fn test_auth_error_neither_client_nor_server() {
409 let e = WasmError::new(ErrorCode::AuthError, "x");
410 assert!(!e.is_client_error());
412 assert!(!e.is_server_error());
413 }
414
415 #[test]
419 fn test_to_json_contains_code() {
420 let e = WasmError::new(ErrorCode::ParseError, "bad syntax");
421 let json = e.to_json();
422 assert!(json.contains("\"code\":1"), "json={json}");
423 }
424
425 #[test]
426 fn test_to_json_contains_message() {
427 let e = WasmError::new(ErrorCode::NotFound, "resource missing");
428 let json = e.to_json();
429 assert!(json.contains("resource missing"), "json={json}");
430 }
431
432 #[test]
433 fn test_to_json_no_details_field_absent() {
434 let e = WasmError::new(ErrorCode::ParseError, "x");
435 let json = e.to_json();
436 assert!(!json.contains("\"details\""), "json={json}");
437 }
438
439 #[test]
440 fn test_to_json_with_details() {
441 let e = WasmError::new(ErrorCode::ServerError, "crash").with_details("line 42");
442 let json = e.to_json();
443 assert!(json.contains("\"details\""), "json={json}");
444 assert!(json.contains("line 42"), "json={json}");
445 }
446
447 #[test]
448 fn test_to_json_escapes_quotes() {
449 let e = WasmError::new(ErrorCode::ParseError, r#"He said "hello""#);
450 let json = e.to_json();
451 assert!(json.contains(r#"\""#), "json={json}");
452 }
453
454 #[test]
455 fn test_to_json_recoverable_true() {
456 let json = WasmError::new(ErrorCode::InvalidInput, "x").to_json();
457 assert!(json.contains("\"recoverable\":true"), "json={json}");
458 }
459
460 #[test]
461 fn test_to_json_recoverable_false() {
462 let json = WasmError::new(ErrorCode::ServerError, "x")
463 .unrecoverable()
464 .to_json();
465 assert!(json.contains("\"recoverable\":false"), "json={json}");
466 }
467
468 #[test]
472 fn test_handler_new_empty() {
473 let h = ErrorHandler::new(10);
474 assert_eq!(h.error_count(), 0);
475 }
476
477 #[test]
478 fn test_handler_handle_increments_count() {
479 let mut h = ErrorHandler::new(10);
480 h.handle(WasmError::new(ErrorCode::ParseError, "e1"));
481 assert_eq!(h.error_count(), 1);
482 }
483
484 #[test]
485 fn test_handler_handle_returns_json() {
486 let mut h = ErrorHandler::new(10);
487 let json = h.handle(WasmError::new(ErrorCode::NotFound, "missing"));
488 assert!(json.contains("\"code\":4"), "json={json}");
489 }
490
491 #[test]
492 fn test_handler_last_error() {
493 let mut h = ErrorHandler::new(10);
494 h.handle(WasmError::new(ErrorCode::ParseError, "first"));
495 h.handle(WasmError::new(ErrorCode::NotFound, "second"));
496 let last = h.last_error().expect("some last error");
497 assert_eq!(last.message, "second");
498 }
499
500 #[test]
501 fn test_handler_last_error_empty() {
502 let h = ErrorHandler::new(10);
503 assert!(h.last_error().is_none());
504 }
505
506 #[test]
507 fn test_handler_eviction_when_full() {
508 let mut h = ErrorHandler::new(3);
509 for i in 0..5u32 {
510 h.handle(WasmError::new(ErrorCode::ParseError, format!("e{i}")));
511 }
512 assert_eq!(h.error_count(), 3);
513 }
514
515 #[test]
516 fn test_handler_has_unrecoverable_false() {
517 let mut h = ErrorHandler::new(10);
518 h.handle(WasmError::new(ErrorCode::ParseError, "x"));
519 assert!(!h.has_unrecoverable());
520 }
521
522 #[test]
523 fn test_handler_has_unrecoverable_true() {
524 let mut h = ErrorHandler::new(10);
525 h.handle(WasmError::new(ErrorCode::ServerError, "fatal").unrecoverable());
526 assert!(h.has_unrecoverable());
527 }
528
529 #[test]
530 fn test_handler_clear_history() {
531 let mut h = ErrorHandler::new(10);
532 h.handle(WasmError::new(ErrorCode::ParseError, "x"));
533 h.clear_history();
534 assert_eq!(h.error_count(), 0);
535 }
536
537 #[test]
538 fn test_handler_errors_by_code() {
539 let mut h = ErrorHandler::new(20);
540 h.handle(WasmError::new(ErrorCode::ParseError, "p1"));
541 h.handle(WasmError::new(ErrorCode::NotFound, "n1"));
542 h.handle(WasmError::new(ErrorCode::ParseError, "p2"));
543 let parse_errs = h.errors_by_code(1);
544 assert_eq!(parse_errs.len(), 2);
545 }
546
547 #[test]
548 fn test_handler_errors_by_code_none() {
549 let h = ErrorHandler::new(10);
550 assert!(h.errors_by_code(1).is_empty());
551 }
552
553 #[test]
554 fn test_display_impl() {
555 let e = WasmError::new(ErrorCode::Timeout, "took too long");
556 let s = e.to_string();
557 assert!(s.contains("Timeout"), "display={s}");
558 assert!(s.contains("took too long"), "display={s}");
559 }
560
561 #[test]
562 fn test_wasm_error_is_std_error() {
563 let e: Box<dyn std::error::Error> = Box::new(WasmError::new(ErrorCode::ParseError, "x"));
564 assert!(e.to_string().contains("ParseError"));
565 }
566}