lxy 0.1.1

A convenient async http and RPC framework in Rust
Documentation
use serde::Serialize;

use super::{Error, ErrorCode};

/// Trait for user-defined error types that are bound to specific error codes.
///
/// Implementing this trait ensures that error types are always associated
/// with the correct error code at compile time, preventing misuse and
/// providing type safety.
///
/// # Usage
///
/// Instead of implementing this trait manually, use the [`define_error!`] macro:
///
/// ```
/// use lxy::{define_error, error::ErrorCode};
///
/// define_error!(ErrorCode::ResourceNotFound, UserNotFound);
/// define_error!(ErrorCode::InvalidInput, InvalidEmail);
/// ```
///
/// Then create errors using the generated methods:
///
/// ```
/// use lxy::{define_error, error::{ErrorCode, TypedError}};
///
/// define_error!(ErrorCode::ResourceNotFound, UserNotFound);
///
/// let error = UserNotFound::error("User with ID 123 not found");
/// ```
pub trait TypedError: Sized {
  /// The error code this type is bound to.
  const CODE: ErrorCode;

  /// The string identifier for this error type.
  const TYPE: &'static str;

  /// Creates an error instance with the bound code and type.
  ///
  /// # Examples
  ///
  /// ```
  /// use lxy::{define_error, error::{ErrorCode, TypedError}};
  ///
  /// define_error!(ErrorCode::ResourceNotFound, UserNotFound);
  ///
  /// let error = UserNotFound::error("User not found");
  /// assert_eq!(error.error_type(), "UserNotFound");
  /// ```
  fn error(message: impl Into<String>) -> Error {
    Error::new(Self::CODE, Self::TYPE, message)
  }

  /// Creates an error with additional structured data.
  ///
  /// # Examples
  ///
  /// ```
  /// use lxy::{define_error, error::{ErrorCode, TypedError}};
  /// use serde_json::json;
  ///
  /// define_error!(ErrorCode::ResourceNotFound, UserNotFound);
  ///
  /// let error = UserNotFound::error_with_data(
  ///     "User not found",
  ///     json!({"user_id": 123})
  /// );
  /// assert!(error.data().is_some());
  /// ```
  fn error_with_data<T: Serialize>(message: impl Into<String>, data: T) -> Error {
    Error::new(Self::CODE, Self::TYPE, message).with_data(data)
  }

  /// Creates an error with a source error for error chaining.
  ///
  /// # Examples
  ///
  /// ```
  /// use lxy::{define_error, error::{ErrorCode, TypedError}};
  /// use std::io;
  ///
  /// define_error!(ErrorCode::Internal, ConfigLoadFailed);
  ///
  /// let io_error = io::Error::new(io::ErrorKind::NotFound, "config.toml not found");
  /// let error = ConfigLoadFailed::error_with_source(
  ///     "Failed to load configuration",
  ///     io_error
  /// );
  /// assert!(error.source().is_some());
  /// ```
  fn error_with_source<E>(message: impl Into<String>, source: E) -> Error
  where
    E: std::error::Error + Send + Sync + 'static,
  {
    Error::new(Self::CODE, Self::TYPE, message).with_source(source)
  }
}

/// Defines a typed error with compile-time code binding.
///
/// This macro generates a zero-sized type that implements [`TypedError`],
/// ensuring that the error type is always used with the correct error code.
///
/// # Syntax
///
/// ```text
/// define_error!(ErrorCode::SomeCode, ErrorName);
/// ```
///
/// The generated error type string is always `stringify!(ErrorName)`.
///
/// # Examples
///
/// ```
/// use lxy::{define_error, error::ErrorCode};
///
/// define_error!(ErrorCode::ResourceNotFound, UserNotFound);
/// define_error!(ErrorCode::InvalidInput, InvalidEmail);
/// define_error!(ErrorCode::Unauthorized, Unauthorized);
/// ```
///
/// ## Using the Defined Errors
///
/// ```
/// use lxy::{define_error, error::{ErrorCode, Result, TypedError}};
///
/// define_error!(ErrorCode::ResourceNotFound, UserNotFound);
///
/// struct User { id: u32 }
///
/// fn find_user(id: u32) -> Option<User> { None }
///
/// fn get_user(id: u32) -> Result<User> {
///     find_user(id)
///         .ok_or_else(|| UserNotFound::error(format!("User {} not found", id)))
/// }
/// ```
///
/// ## With Additional Data
///
/// ```
/// use lxy::{define_error, error::{ErrorCode, TypedError}};
/// use serde_json::json;
///
/// define_error!(ErrorCode::RateLimited, RateLimitExceeded);
///
/// let error = RateLimitExceeded::error_with_data(
///     "Too many requests",
///     json!({
///         "retry_after": 60,
///         "limit": 100
///     })
/// );
/// ```
#[macro_export]
macro_rules! define_error {
  ($code:expr, $name:ident) => {
    #[derive(Debug, Clone, Copy)]
    pub struct $name;

    impl $crate::error::TypedError for $name {
      const CODE: $crate::error::ErrorCode = $code;
      const TYPE: &'static str = stringify!($name);
    }

    impl From<$name> for $crate::error::Error {
      fn from(_: $name) -> Self {
        Self::new($code, stringify!($name), "")
      }
    }
  };
  ($name:ident, $code:expr) => {
    compile_error!(
      "define_error! argument order changed. Use define_error!(ErrorCode::SomeCode, ErrorName)."
    );
  };
  ($name:ident, $code:expr, $type:expr) => {
    compile_error!(
      "define_error! no longer accepts a custom type string. Use define_error!(ErrorCode::SomeCode, ErrorName)."
    );
  };
  ($code:expr, $name:ident, $type:expr) => {
    compile_error!(
      "define_error! no longer accepts a custom type string. Use define_error!(ErrorCode::SomeCode, ErrorName)."
    );
  };
}

/// Batch defines multiple typed errors under the same error code.
///
/// This macro allows you to define multiple error types that share the same error code,
/// making it more convenient to organize related errors. Each error type automatically
/// implements `From` for `Error`, allowing seamless conversion.
///
/// # Syntax
///
/// ```text
/// define_errors! {
///     ErrorCode::SomeCode => [
///         ErrorType1,
///         ErrorType2,
///         ErrorType3,
///     ],
///     ErrorCode::AnotherCode => [
///         ErrorType4,
///     ],
/// }
/// ```
///
/// # Examples
///
/// ```
/// use lxy::{define_errors, error::{ErrorCode, Result, Error}};
///
/// define_errors! {
///     ErrorCode::ResourceNotFound => [
///         UserNotFound,
///         ProjectNotFound,
///         FileNotFound,
///     ],
///     ErrorCode::InvalidInput => [
///         InvalidEmail,
///         InvalidPassword,
///     ],
/// }
/// ```
///
/// ## Using with Direct Conversion
///
/// ```
/// # use lxy::{define_errors, error::{ErrorCode, Result, Error}};
/// # define_errors! {
/// #     ErrorCode::ResourceNotFound => [UserNotFound],
/// # }
/// fn get_user(id: u32) -> Result<String> {
///     if id == 0 {
///         return Err(UserNotFound.into());
///     }
///     Ok("User found".to_string())
/// }
/// ```
///
/// ## Using with Custom Messages
///
/// ```
/// use lxy::{define_errors, error::{ErrorCode, Result, TypedError}};
///
/// define_errors! {
///     ErrorCode::ResourceNotFound => [UserNotFound],
/// }
///
/// fn get_user(id: u32) -> Result<String> {
///     if id == 0 {
///         return Err(UserNotFound::error(format!("User {} not found", id)));
///     }
///     Ok("User found".to_string())
/// }
/// ```
#[macro_export]
macro_rules! define_errors {
  (
    $(
      $code:expr => [
        $( $name:ident ),+ $(,)?
      ]
    ),+ $(,)?
  ) => {
    $(
      $(
        #[derive(Debug, Clone, Copy)]
        pub struct $name;

        impl $crate::error::TypedError for $name {
          const CODE: $crate::error::ErrorCode = $code;
          const TYPE: &'static str = stringify!($name);
        }

        impl From<$name> for $crate::error::Error {
          fn from(_: $name) -> Self {
            Self::new($code, stringify!($name), "")
          }
        }
      )+
    )+
  };
}