lxy 0.1.1

A convenient async http and RPC framework in Rust
Documentation
use serde::{Deserialize, Serialize};
use std::fmt;

use super::ErrorCode;

/// Core error type for the lxy framework.
///
/// All errors in lxy follow this structure with these fields:
/// - `code`: System-defined error code category
/// - `type`: User-defined string for fine-grained categorization
/// - `message`: Human-readable error message
/// - `data`: Optional additional structured data
/// - `source`: Optional source error for error chaining
///
/// In debug builds, errors automatically capture backtraces to aid in debugging.
///
/// # Examples
///
/// ```
/// use lxy::error::{Error, ErrorCode};
///
/// let error = Error::new(
///     ErrorCode::ResourceNotFound,
///     "user_not_found",
///     "User with ID 123 not found"
/// );
///
/// assert_eq!(error.code(), ErrorCode::ResourceNotFound);
/// assert_eq!(error.error_type(), "user_not_found");
/// assert_eq!(error.message(), "User with ID 123 not found");
/// ```
///
/// ## With Additional Data
///
/// ```
/// use lxy::error::{Error, ErrorCode};
/// use serde_json::json;
///
/// let error = Error::new(
///     ErrorCode::RateLimited,
///     "rate_limit_exceeded",
///     "Too many requests"
/// ).with_data(json!({
///     "retry_after": 60,
///     "limit": 100
/// }));
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Error {
  #[serde(flatten)]
  err: Box<ErrorImpl>,
}

#[derive(Debug, Serialize, Deserialize)]
struct ErrorImpl {
  code: ErrorCode,

  #[serde(rename = "type")]
  error_type: String,

  message: String,

  #[serde(skip_serializing_if = "Option::is_none")]
  data: Option<serde_json::Value>,

  #[serde(skip)]
  source: Option<Box<dyn std::error::Error + Send + Sync>>,

  #[serde(skip)]
  #[cfg(debug_assertions)]
  backtrace: Option<std::backtrace::Backtrace>,
}

impl Clone for ErrorImpl {
  fn clone(&self) -> Self {
    Self {
      code: self.code,
      error_type: self.error_type.clone(),
      message: self.message.clone(),
      data: self.data.clone(),
      source: None,
      #[cfg(debug_assertions)]
      backtrace: None,
    }
  }
}

impl Error {
  /// Creates a new error with the given code, type, and message.
  ///
  /// In debug builds, this automatically captures a backtrace.
  ///
  /// # Examples
  ///
  /// ```
  /// use lxy::error::{Error, ErrorCode};
  ///
  /// let error = Error::new(
  ///     ErrorCode::InvalidInput,
  ///     "invalid_email",
  ///     "Email address is not valid"
  /// );
  /// ```
  pub fn new(code: ErrorCode, error_type: impl Into<String>, message: impl Into<String>) -> Self {
    Self {
      err: Box::new(ErrorImpl {
        code,
        error_type: error_type.into(),
        message: message.into(),
        data: None,
        source: None,
        #[cfg(debug_assertions)]
        backtrace: Some(std::backtrace::Backtrace::capture()),
      }),
    }
  }

  /// Adds additional structured data to the error.
  ///
  /// The data must be serializable to JSON. If serialization fails,
  /// the data field will remain None.
  ///
  /// # Examples
  ///
  /// ```
  /// use lxy::error::{Error, ErrorCode};
  /// use serde_json::json;
  ///
  /// let error = Error::new(
  ///     ErrorCode::RateLimited,
  ///     "rate_limit",
  ///     "Too many requests"
  /// ).with_data(json!({
  ///     "retry_after": 60
  /// }));
  ///
  /// assert!(error.data().is_some());
  /// ```
  pub fn with_data<T: Serialize>(self, data: T) -> Self {
    let mut err = self.err;
    err.data = serde_json::to_value(data).ok();
    Self { err }
  }

  /// Adds a source error to this error for error chaining.
  ///
  /// This is useful for preserving the original error when wrapping it
  /// in a higher-level error type.
  ///
  /// # Examples
  ///
  /// ```
  /// use lxy::error::{Error, ErrorCode};
  /// use std::io;
  ///
  /// let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
  /// let error = Error::new(
  ///     ErrorCode::Internal,
  ///     "config_load_failed",
  ///     "Failed to load configuration"
  /// ).with_source(io_error);
  ///
  /// assert!(error.source().is_some());
  /// ```
  pub fn with_source<E>(self, source: E) -> Self
  where
    E: std::error::Error + Send + Sync + 'static,
  {
    let mut err = self.err;
    err.source = Some(Box::new(source));
    Self { err }
  }

  /// Creates an internal error from any error type that implements `std::error::Error`.
  ///
  /// This is useful for converting standard library errors or third-party errors
  /// into lxy errors. All such errors are categorized as `ErrorCode::Internal`
  /// with the error type "internal_error". The original error is preserved as the source.
  ///
  /// # Examples
  ///
  /// ```
  /// use lxy::error::Error;
  /// use std::io;
  ///
  /// let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
  /// let error = Error::internal(io_error);
  ///
  /// assert_eq!(error.error_type(), "internal_error");
  /// assert!(error.source().is_some());
  /// ```
  pub fn internal<E>(error: E) -> Self
  where
    E: std::error::Error + Send + Sync + 'static,
  {
    Self::new(ErrorCode::Internal, "internal_error", error.to_string()).with_source(error)
  }

  /// Returns the error code.
  pub fn code(&self) -> ErrorCode {
    self.err.code
  }

  /// Returns the error type.
  pub fn error_type(&self) -> &str {
    &self.err.error_type
  }

  /// Returns the error message.
  pub fn message(&self) -> &str {
    &self.err.message
  }

  /// Returns the additional data, if any.
  pub fn data(&self) -> Option<&serde_json::Value> {
    self.err.data.as_ref()
  }

  /// Returns the source error, if any.
  ///
  /// # Examples
  ///
  /// ```
  /// use lxy::error::Error;
  /// use std::io;
  ///
  /// let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
  /// let error = Error::internal(io_error);
  ///
  /// assert!(error.source().is_some());
  /// ```
  pub fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
    self
      .err
      .source
      .as_ref()
      .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
  }

  /// Returns the backtrace if captured (debug builds only).
  #[cfg(debug_assertions)]
  pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
    self.err.backtrace.as_ref()
  }

  /// Checks if the error matches the given error code.
  ///
  /// # Examples
  ///
  /// ```
  /// use lxy::error::{Error, ErrorCode};
  ///
  /// let error = Error::new(
  ///     ErrorCode::ResourceNotFound,
  ///     "user_not_found",
  ///     "User not found"
  /// );
  ///
  /// assert!(error.is_code(ErrorCode::ResourceNotFound));
  /// assert!(!error.is_code(ErrorCode::InvalidInput));
  /// ```
  pub fn is_code(&self, code: ErrorCode) -> bool {
    self.err.code == code
  }

  /// Checks if the error matches the given error type.
  ///
  /// This method accepts any type that implements [`TypedError`], providing
  /// compile-time type safety and avoiding string typos.
  ///
  /// # Examples
  ///
  /// ```
  /// use lxy::{define_error, error::{Error, ErrorCode, TypedError}};
  ///
  /// define_error!(ErrorCode::ResourceNotFound, UserNotFound);
  /// define_error!(ErrorCode::ResourceNotFound, ProjectNotFound);
  ///
  /// let error = UserNotFound::error("User not found");
  ///
  /// assert!(error.is(UserNotFound));
  /// assert!(!error.is(ProjectNotFound));
  /// ```
  pub fn is<T: super::TypedError>(&self, _error_type: T) -> bool {
    self.err.error_type == T::TYPE
  }
}

impl fmt::Display for Error {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    write!(
      f,
      "[{}] {}: {}",
      self.err.error_type, self.err.code, self.err.message
    )
  }
}

impl std::error::Error for Error {
  fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
    self
      .err
      .source
      .as_ref()
      .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
  }
}