1use std::sync::atomic::{AtomicUsize, Ordering};
31
32use fastmcp_core::McpError;
33
34use crate::console::FastMcpConsole;
35use crate::diagnostics::RichErrorRenderer;
36
37pub struct ErrorBoundary<'a> {
54 console: &'a FastMcpConsole,
55 renderer: RichErrorRenderer,
56 exit_on_error: bool,
57 error_count: AtomicUsize,
58}
59
60impl<'a> ErrorBoundary<'a> {
61 #[must_use]
74 pub fn new(console: &'a FastMcpConsole) -> Self {
75 Self {
76 console,
77 renderer: RichErrorRenderer::new(),
78 exit_on_error: false,
79 error_count: AtomicUsize::new(0),
80 }
81 }
82
83 #[must_use]
99 pub fn with_exit_on_error(mut self, exit: bool) -> Self {
100 self.exit_on_error = exit;
101 self
102 }
103
104 pub fn wrap<T, E>(&self, result: Result<T, E>) -> Option<T>
124 where
125 E: Into<McpError>,
126 {
127 match result {
128 Ok(value) => Some(value),
129 Err(e) => {
130 let error = e.into();
131 self.handle_error(&error);
132 None
133 }
134 }
135 }
136
137 pub fn wrap_with_context<T, E>(&self, result: Result<T, E>, context: &str) -> Option<T>
153 where
154 E: Into<McpError>,
155 {
156 match result {
157 Ok(value) => Some(value),
158 Err(e) => {
159 let error = e.into();
160 self.console.print(&format!("[dim]Context: {}[/]", context));
161 self.handle_error(&error);
162 None
163 }
164 }
165 }
166
167 pub fn wrap_result<T, E>(&self, result: Result<T, E>) -> Result<T, McpError>
184 where
185 E: Into<McpError>,
186 {
187 match result {
188 Ok(value) => Ok(value),
189 Err(e) => {
190 let error = e.into();
191 self.handle_error(&error);
192 Err(error)
193 }
194 }
195 }
196
197 pub fn wrap_result_with_context<T, E>(
203 &self,
204 result: Result<T, E>,
205 context: &str,
206 ) -> Result<T, McpError>
207 where
208 E: Into<McpError>,
209 {
210 match result {
211 Ok(value) => Ok(value),
212 Err(e) => {
213 let error = e.into();
214 self.console.print(&format!("[dim]Context: {}[/]", context));
215 self.handle_error(&error);
216 Err(error)
217 }
218 }
219 }
220
221 pub fn display_error(&self, error: &McpError) {
234 self.handle_error(error);
235 }
236
237 #[must_use]
242 pub fn error_count(&self) -> usize {
243 self.error_count.load(Ordering::Relaxed)
244 }
245
246 #[must_use]
251 pub fn has_errors(&self) -> bool {
252 self.error_count() > 0
253 }
254
255 pub fn reset_count(&self) {
260 self.error_count.store(0, Ordering::Relaxed);
261 }
262
263 fn handle_error(&self, error: &McpError) {
265 self.error_count.fetch_add(1, Ordering::Relaxed);
266 self.renderer.render(error, self.console);
267
268 if self.exit_on_error {
269 std::process::exit(1);
270 }
271 }
272}
273
274#[macro_export]
291macro_rules! try_display {
292 ($boundary:expr, $expr:expr) => {
293 match $boundary.wrap($expr) {
294 Some(v) => v,
295 None => return,
296 }
297 };
298 ($boundary:expr, $expr:expr, $ctx:expr) => {
299 match $boundary.wrap_with_context($expr, $ctx) {
300 Some(v) => v,
301 None => return,
302 }
303 };
304}
305
306#[macro_export]
322macro_rules! try_display_result {
323 ($boundary:expr, $expr:expr) => {
324 match $boundary.wrap_result($expr) {
325 Ok(v) => v,
326 Err(e) => return Err(e),
327 }
328 };
329 ($boundary:expr, $expr:expr, $ctx:expr) => {
330 match $boundary.wrap_result_with_context($expr, $ctx) {
331 Ok(v) => v,
332 Err(e) => return Err(e),
333 }
334 };
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use fastmcp_core::McpErrorCode;
341
342 fn test_console() -> FastMcpConsole {
343 FastMcpConsole::with_enabled(false)
345 }
346
347 #[test]
348 fn test_error_boundary_wrap_success() {
349 let console = test_console();
350 let boundary = ErrorBoundary::new(&console);
351
352 let result: Result<i32, McpError> = Ok(42);
353 assert_eq!(boundary.wrap(result), Some(42));
354 assert_eq!(boundary.error_count(), 0);
355 assert!(!boundary.has_errors());
356 }
357
358 #[test]
359 fn test_error_boundary_wrap_error() {
360 let console = test_console();
361 let boundary = ErrorBoundary::new(&console);
362
363 let result: Result<i32, McpError> = Err(McpError::internal_error("test error"));
364 assert_eq!(boundary.wrap(result), None);
365 assert_eq!(boundary.error_count(), 1);
366 assert!(boundary.has_errors());
367 }
368
369 #[test]
370 fn test_error_boundary_wrap_with_context() {
371 let console = test_console();
372 let boundary = ErrorBoundary::new(&console);
373
374 let result: Result<i32, McpError> = Err(McpError::internal_error("test"));
375 assert_eq!(boundary.wrap_with_context(result, "Loading config"), None);
376 assert_eq!(boundary.error_count(), 1);
377 }
378
379 #[test]
380 fn test_error_boundary_wrap_result_success() {
381 let console = test_console();
382 let boundary = ErrorBoundary::new(&console);
383
384 let result: Result<i32, McpError> = Ok(42);
385 let wrapped = boundary.wrap_result(result);
386 assert!(wrapped.is_ok());
387 assert_eq!(wrapped.unwrap(), 42);
388 assert_eq!(boundary.error_count(), 0);
389 }
390
391 #[test]
392 fn test_error_boundary_wrap_result_error() {
393 let console = test_console();
394 let boundary = ErrorBoundary::new(&console);
395
396 let result: Result<i32, McpError> = Err(McpError::internal_error("test"));
397 let wrapped = boundary.wrap_result(result);
398 assert!(wrapped.is_err());
399 assert_eq!(wrapped.unwrap_err().code, McpErrorCode::InternalError);
400 assert_eq!(boundary.error_count(), 1);
401 }
402
403 #[test]
404 fn test_error_boundary_multiple_errors() {
405 let console = test_console();
406 let boundary = ErrorBoundary::new(&console);
407
408 let err1: Result<i32, McpError> = Err(McpError::internal_error("error 1"));
409 let err2: Result<i32, McpError> = Err(McpError::parse_error("error 2"));
410 let err3: Result<i32, McpError> = Err(McpError::method_not_found("test"));
411
412 boundary.wrap(err1);
413 boundary.wrap(err2);
414 boundary.wrap(err3);
415
416 assert_eq!(boundary.error_count(), 3);
417 }
418
419 #[test]
420 fn test_error_boundary_reset_count() {
421 let console = test_console();
422 let boundary = ErrorBoundary::new(&console);
423
424 let err: Result<i32, McpError> = Err(McpError::internal_error("test"));
425 boundary.wrap(err);
426 assert_eq!(boundary.error_count(), 1);
427
428 boundary.reset_count();
429 assert_eq!(boundary.error_count(), 0);
430 assert!(!boundary.has_errors());
431 }
432
433 #[test]
434 fn test_error_boundary_display_error() {
435 let console = test_console();
436 let boundary = ErrorBoundary::new(&console);
437
438 let error = McpError::internal_error("direct display");
439 boundary.display_error(&error);
440
441 assert_eq!(boundary.error_count(), 1);
442 }
443
444 #[test]
445 fn test_error_boundary_mixed_results() {
446 let console = test_console();
447 let boundary = ErrorBoundary::new(&console);
448
449 let ok1: Result<i32, McpError> = Ok(1);
451 let ok2: Result<i32, McpError> = Ok(2);
452
453 let err1: Result<i32, McpError> = Err(McpError::internal_error("e1"));
455 let err2: Result<i32, McpError> = Err(McpError::internal_error("e2"));
456
457 assert_eq!(boundary.wrap(ok1), Some(1));
458 assert_eq!(boundary.wrap(err1), None);
459 assert_eq!(boundary.wrap(ok2), Some(2));
460 assert_eq!(boundary.wrap(err2), None);
461
462 assert_eq!(boundary.error_count(), 2);
464 }
465
466 #[test]
467 fn test_error_boundary_from_other_error_types() {
468 let console = test_console();
469 let boundary = ErrorBoundary::new(&console);
470
471 let json_result: Result<serde_json::Value, serde_json::Error> =
473 serde_json::from_str("invalid json");
474
475 let mcp_result = json_result.map_err(McpError::from);
477 assert_eq!(boundary.wrap(mcp_result), None);
478 assert_eq!(boundary.error_count(), 1);
479 }
480
481 #[test]
482 fn test_with_exit_on_error_builder_flag() {
483 let console = test_console();
484
485 let boundary = ErrorBoundary::new(&console).with_exit_on_error(false);
487 let result: Result<i32, McpError> = Ok(7);
488 assert_eq!(boundary.wrap(result), Some(7));
489 assert_eq!(boundary.error_count(), 0);
490 }
491
492 #[test]
493 fn test_wrap_with_context_success_path() {
494 let console = test_console();
495 let boundary = ErrorBoundary::new(&console);
496
497 let result: Result<&str, McpError> = Ok("ok");
498 assert_eq!(
499 boundary.wrap_with_context(result, "unused context"),
500 Some("ok")
501 );
502 assert_eq!(boundary.error_count(), 0);
503 }
504
505 #[test]
506 fn test_wrap_result_with_context_success_and_error_paths() {
507 let console = test_console();
508 let boundary = ErrorBoundary::new(&console);
509
510 let ok: Result<i32, McpError> = Ok(123);
511 let wrapped_ok = boundary.wrap_result_with_context(ok, "computing value");
512 assert!(wrapped_ok.is_ok());
513 assert_eq!(wrapped_ok.unwrap(), 123);
514 assert_eq!(boundary.error_count(), 0);
515
516 let err: Result<i32, McpError> = Err(McpError::internal_error("boom"));
517 let wrapped = boundary.wrap_result_with_context(err, "computing value");
518 assert!(wrapped.is_err());
519 assert_eq!(wrapped.unwrap_err().code, McpErrorCode::InternalError);
520 assert_eq!(boundary.error_count(), 1);
521 }
522}