use std::sync::atomic::{AtomicUsize, Ordering};
use fastmcp_core::McpError;
use crate::console::FastMcpConsole;
use crate::diagnostics::RichErrorRenderer;
pub struct ErrorBoundary<'a> {
console: &'a FastMcpConsole,
renderer: RichErrorRenderer,
exit_on_error: bool,
error_count: AtomicUsize,
}
impl<'a> ErrorBoundary<'a> {
#[must_use]
pub fn new(console: &'a FastMcpConsole) -> Self {
Self {
console,
renderer: RichErrorRenderer::new(),
exit_on_error: false,
error_count: AtomicUsize::new(0),
}
}
#[must_use]
pub fn with_exit_on_error(mut self, exit: bool) -> Self {
self.exit_on_error = exit;
self
}
pub fn wrap<T, E>(&self, result: Result<T, E>) -> Option<T>
where
E: Into<McpError>,
{
match result {
Ok(value) => Some(value),
Err(e) => {
let error = e.into();
self.handle_error(&error);
None
}
}
}
pub fn wrap_with_context<T, E>(&self, result: Result<T, E>, context: &str) -> Option<T>
where
E: Into<McpError>,
{
match result {
Ok(value) => Some(value),
Err(e) => {
let error = e.into();
self.console.print(&format!("[dim]Context: {}[/]", context));
self.handle_error(&error);
None
}
}
}
pub fn wrap_result<T, E>(&self, result: Result<T, E>) -> Result<T, McpError>
where
E: Into<McpError>,
{
match result {
Ok(value) => Ok(value),
Err(e) => {
let error = e.into();
self.handle_error(&error);
Err(error)
}
}
}
pub fn wrap_result_with_context<T, E>(
&self,
result: Result<T, E>,
context: &str,
) -> Result<T, McpError>
where
E: Into<McpError>,
{
match result {
Ok(value) => Ok(value),
Err(e) => {
let error = e.into();
self.console.print(&format!("[dim]Context: {}[/]", context));
self.handle_error(&error);
Err(error)
}
}
}
pub fn display_error(&self, error: &McpError) {
self.handle_error(error);
}
#[must_use]
pub fn error_count(&self) -> usize {
self.error_count.load(Ordering::Relaxed)
}
#[must_use]
pub fn has_errors(&self) -> bool {
self.error_count() > 0
}
pub fn reset_count(&self) {
self.error_count.store(0, Ordering::Relaxed);
}
fn handle_error(&self, error: &McpError) {
self.error_count.fetch_add(1, Ordering::Relaxed);
self.renderer.render(error, self.console);
if self.exit_on_error {
std::process::exit(1);
}
}
}
#[macro_export]
macro_rules! try_display {
($boundary:expr, $expr:expr) => {
match $boundary.wrap($expr) {
Some(v) => v,
None => return,
}
};
($boundary:expr, $expr:expr, $ctx:expr) => {
match $boundary.wrap_with_context($expr, $ctx) {
Some(v) => v,
None => return,
}
};
}
#[macro_export]
macro_rules! try_display_result {
($boundary:expr, $expr:expr) => {
match $boundary.wrap_result($expr) {
Ok(v) => v,
Err(e) => return Err(e),
}
};
($boundary:expr, $expr:expr, $ctx:expr) => {
match $boundary.wrap_result_with_context($expr, $ctx) {
Ok(v) => v,
Err(e) => return Err(e),
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use fastmcp_core::McpErrorCode;
fn test_console() -> FastMcpConsole {
FastMcpConsole::with_enabled(false)
}
#[test]
fn test_error_boundary_wrap_success() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let result: Result<i32, McpError> = Ok(42);
assert_eq!(boundary.wrap(result), Some(42));
assert_eq!(boundary.error_count(), 0);
assert!(!boundary.has_errors());
}
#[test]
fn test_error_boundary_wrap_error() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let result: Result<i32, McpError> = Err(McpError::internal_error("test error"));
assert_eq!(boundary.wrap(result), None);
assert_eq!(boundary.error_count(), 1);
assert!(boundary.has_errors());
}
#[test]
fn test_error_boundary_wrap_with_context() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let result: Result<i32, McpError> = Err(McpError::internal_error("test"));
assert_eq!(boundary.wrap_with_context(result, "Loading config"), None);
assert_eq!(boundary.error_count(), 1);
}
#[test]
fn test_error_boundary_wrap_result_success() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let result: Result<i32, McpError> = Ok(42);
let wrapped = boundary.wrap_result(result);
assert!(wrapped.is_ok());
assert_eq!(wrapped.unwrap(), 42);
assert_eq!(boundary.error_count(), 0);
}
#[test]
fn test_error_boundary_wrap_result_error() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let result: Result<i32, McpError> = Err(McpError::internal_error("test"));
let wrapped = boundary.wrap_result(result);
assert!(wrapped.is_err());
assert_eq!(wrapped.unwrap_err().code, McpErrorCode::InternalError);
assert_eq!(boundary.error_count(), 1);
}
#[test]
fn test_error_boundary_multiple_errors() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let err1: Result<i32, McpError> = Err(McpError::internal_error("error 1"));
let err2: Result<i32, McpError> = Err(McpError::parse_error("error 2"));
let err3: Result<i32, McpError> = Err(McpError::method_not_found("test"));
boundary.wrap(err1);
boundary.wrap(err2);
boundary.wrap(err3);
assert_eq!(boundary.error_count(), 3);
}
#[test]
fn test_error_boundary_reset_count() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let err: Result<i32, McpError> = Err(McpError::internal_error("test"));
boundary.wrap(err);
assert_eq!(boundary.error_count(), 1);
boundary.reset_count();
assert_eq!(boundary.error_count(), 0);
assert!(!boundary.has_errors());
}
#[test]
fn test_error_boundary_display_error() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let error = McpError::internal_error("direct display");
boundary.display_error(&error);
assert_eq!(boundary.error_count(), 1);
}
#[test]
fn test_error_boundary_mixed_results() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let ok1: Result<i32, McpError> = Ok(1);
let ok2: Result<i32, McpError> = Ok(2);
let err1: Result<i32, McpError> = Err(McpError::internal_error("e1"));
let err2: Result<i32, McpError> = Err(McpError::internal_error("e2"));
assert_eq!(boundary.wrap(ok1), Some(1));
assert_eq!(boundary.wrap(err1), None);
assert_eq!(boundary.wrap(ok2), Some(2));
assert_eq!(boundary.wrap(err2), None);
assert_eq!(boundary.error_count(), 2);
}
#[test]
fn test_error_boundary_from_other_error_types() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let json_result: Result<serde_json::Value, serde_json::Error> =
serde_json::from_str("invalid json");
let mcp_result = json_result.map_err(McpError::from);
assert_eq!(boundary.wrap(mcp_result), None);
assert_eq!(boundary.error_count(), 1);
}
#[test]
fn test_with_exit_on_error_builder_flag() {
let console = test_console();
let boundary = ErrorBoundary::new(&console).with_exit_on_error(false);
let result: Result<i32, McpError> = Ok(7);
assert_eq!(boundary.wrap(result), Some(7));
assert_eq!(boundary.error_count(), 0);
}
#[test]
fn test_wrap_with_context_success_path() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let result: Result<&str, McpError> = Ok("ok");
assert_eq!(
boundary.wrap_with_context(result, "unused context"),
Some("ok")
);
assert_eq!(boundary.error_count(), 0);
}
#[test]
fn test_wrap_result_with_context_success_and_error_paths() {
let console = test_console();
let boundary = ErrorBoundary::new(&console);
let ok: Result<i32, McpError> = Ok(123);
let wrapped_ok = boundary.wrap_result_with_context(ok, "computing value");
assert!(wrapped_ok.is_ok());
assert_eq!(wrapped_ok.unwrap(), 123);
assert_eq!(boundary.error_count(), 0);
let err: Result<i32, McpError> = Err(McpError::internal_error("boom"));
let wrapped = boundary.wrap_result_with_context(err, "computing value");
assert!(wrapped.is_err());
assert_eq!(wrapped.unwrap_err().code, McpErrorCode::InternalError);
assert_eq!(boundary.error_count(), 1);
}
}