Skip to main content

nexcore_error/
error.rs

1//! Core error type implementation.
2
3#[cfg(not(feature = "std"))]
4use alloc::{boxed::Box, string::String};
5
6#[cfg(feature = "std")]
7use std::{boxed::Box, string::String};
8
9use core::fmt;
10
11/// A type-erased error that can hold any error type.
12///
13/// Equivalent to `anyhow::Error`.
14pub struct NexError {
15    inner: Box<dyn fmt::Display + Send + Sync + 'static>,
16    #[cfg(feature = "std")]
17    source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
18}
19
20impl NexError {
21    /// Creates a new error from a displayable message.
22    #[must_use]
23    pub fn msg<M: fmt::Display + Send + Sync + 'static>(message: M) -> Self {
24        Self {
25            inner: Box::new(message),
26            #[cfg(feature = "std")]
27            source: None,
28        }
29    }
30
31    /// Creates a new error from a string.
32    #[must_use]
33    pub fn new(message: impl Into<String>) -> Self {
34        Self::msg(message.into())
35    }
36
37    /// Wraps an existing error with additional context.
38    #[cfg(feature = "std")]
39    #[must_use]
40    pub fn context<C: fmt::Display + Send + Sync + 'static>(self, ctx: C) -> Self {
41        Self {
42            inner: Box::new(format!("{}: {}", ctx, self.inner)),
43            source: self.source,
44        }
45    }
46
47    /// Returns the wrapped error if it was created with context.
48    #[cfg(feature = "std")]
49    #[must_use]
50    pub fn source(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
51        self.source.as_deref()
52    }
53
54    /// Creates a new error from any `std::error::Error` with context.
55    #[cfg(feature = "std")]
56    #[must_use]
57    pub fn from_err<E, C>(err: E, ctx: C) -> Self
58    where
59        E: std::error::Error + Send + Sync + 'static,
60        C: fmt::Display + Send + Sync + 'static,
61    {
62        Self {
63            inner: Box::new(format!("{ctx}: {err}")),
64            source: Some(Box::new(err)),
65        }
66    }
67}
68
69impl fmt::Display for NexError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "{}", self.inner)
72    }
73}
74
75impl fmt::Debug for NexError {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        write!(f, "NexError({})", self.inner)?;
78        #[cfg(feature = "std")]
79        if let Some(ref src) = self.source {
80            write!(f, "\n  caused by: {src}")?;
81        }
82        Ok(())
83    }
84}
85
86#[cfg(feature = "std")]
87impl<E> From<E> for NexError
88where
89    E: std::error::Error + Send + Sync + 'static,
90{
91    fn from(err: E) -> Self {
92        Self {
93            inner: Box::new(err.to_string()),
94            source: Some(Box::new(err)),
95        }
96    }
97}
98
99// ── String interop ──────────────────────────────────────────────
100// These conversions let NexError participate in String-based error
101// pipelines (MCP tool boundaries, JSON serialization, Cow<str>)
102// without adding serde as a dependency.
103
104impl From<NexError> for String {
105    fn from(err: NexError) -> Self {
106        err.to_string()
107    }
108}
109
110#[cfg(not(feature = "std"))]
111impl From<NexError> for alloc::borrow::Cow<'static, str> {
112    fn from(err: NexError) -> Self {
113        alloc::borrow::Cow::Owned(err.to_string())
114    }
115}
116
117#[cfg(feature = "std")]
118impl From<NexError> for std::borrow::Cow<'static, str> {
119    fn from(err: NexError) -> Self {
120        std::borrow::Cow::Owned(err.to_string())
121    }
122}
123
124/// Allows NexError to be used in `json!({"error": e})` contexts
125/// via serde's `Display`-based serialization. Gated behind `serde` feature.
126#[cfg(feature = "serde")]
127impl serde::Serialize for NexError {
128    fn serialize<S: serde::Serializer>(
129        &self,
130        serializer: S,
131    ) -> core::result::Result<S::Ok, S::Error> {
132        serializer.serialize_str(&self.to_string())
133    }
134}