lxy 0.1.1

A convenient async http and RPC framework in Rust
Documentation
//! Error handling integration for HTTP.
//!
//! This module provides conversion from lxy errors to HTTP responses.

use axum::{
  Json,
  response::{IntoResponse, Response},
};

use crate::error::{Error, Result};

/// Converts an lxy error into an HTTP response.
///
/// This implementation allows lxy errors to be returned directly from axum handlers.
/// The error is converted to a JSON response with the appropriate HTTP status code.
///
/// # Response Format
///
/// ```json
/// {
///   "error": {
///     "code": "ResourceNotFound",
///     "type": "UserNotFound",
///     "message": "User 123 not found",
///     "data": null
///   }
/// }
/// ```
///
/// # Examples
///
/// ```
/// use lxy::{define_error, error::{Result, ErrorCode, TypedError}};
/// use axum::{Json, extract::Path};
/// use serde::Serialize;
///
/// define_error!(ErrorCode::ResourceNotFound, UserNotFound);
///
/// #[derive(Serialize)]
/// struct User { name: String }
///
/// async fn find_user(id: u32) -> Option<User> { None }
///
/// async fn get_user(Path(id): Path<u32>) -> Result<Json<User>> {
///     let user = find_user(id)
///         .await
///         .ok_or_else(|| UserNotFound::error(format!("User {} not found", id)))?;
///     Ok(Json(user))
/// }
/// ```
impl IntoResponse for Error {
  fn into_response(self) -> Response {
    let status = self.code().to_http_status();

    let body = Json(serde_json::json!({
        "error": {
            "code": self.code().to_string(),
            "type": self.error_type(),
            "message": self.message(),
            "data": self.data(),
        }
    }));

    (status, body).into_response()
  }
}

/// A convenient `Result` type for HTTP handlers.
///
/// This is an alias for [`Result<T, Error>`](Result) and is provided
/// for clarity when writing HTTP handlers.
///
/// # Examples
///
/// ```
/// use lxy::{define_error, error::{ErrorCode, TypedError}};
/// use lxy::http::HttpResult;
/// use axum::Json;
/// use serde::{Deserialize, Serialize};
///
/// define_error!(ErrorCode::InvalidInput, ValidationError);
///
/// #[derive(Deserialize)]
/// struct CreateUserRequest { email: String }
///
/// #[derive(Serialize)]
/// struct User { id: u32 }
///
/// async fn create_user(Json(req): Json<CreateUserRequest>) -> HttpResult<Json<User>> {
///     if !req.email.contains('@') {
///         return Err(ValidationError::error("Invalid email address"));
///     }
///     Ok(Json(User { id: 1 }))
/// }
/// ```
pub type HttpResult<T> = Result<T>;

#[cfg(test)]
mod tests {
  use super::*;
  use crate::{
    define_error, define_errors,
    error::{ErrorCode, TypedError},
  };
  use axum::{http::StatusCode, response::IntoResponse};
  use http_body_util::BodyExt;
  use serde_json::{Value, json};

  define_error!(ErrorCode::ResourceNotFound, UserNotFound);
  define_error!(ErrorCode::InvalidInput, InvalidEmail);
  define_error!(ErrorCode::RateLimited, RateLimitError);

  #[tokio::test]
  async fn test_error_into_response() {
    let error = UserNotFound::error("User 123 not found");
    let response = error.into_response();

    assert_eq!(response.status(), StatusCode::NOT_FOUND);

    let body = response.into_body().collect().await.unwrap().to_bytes();
    let json: Value = serde_json::from_slice(&body).unwrap();

    assert_eq!(json["error"]["code"], "ResourceNotFound");
    assert_eq!(json["error"]["type"], "UserNotFound");
    assert_eq!(json["error"]["message"], "User 123 not found");
    assert!(json["error"]["data"].is_null());
  }

  #[tokio::test]
  async fn test_with_data_into_response() {
    let error = RateLimitError::error_with_data(
      "Too many requests",
      json!({
          "retry_after": 60,
          "limit": 100
      }),
    );
    let response = error.into_response();

    assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);

    let body = response.into_body().collect().await.unwrap().to_bytes();
    let json: Value = serde_json::from_slice(&body).unwrap();

    assert_eq!(json["error"]["code"], "RateLimited");
    assert_eq!(json["error"]["type"], "RateLimitError");
    assert_eq!(json["error"]["data"]["retry_after"], 60);
    assert_eq!(json["error"]["data"]["limit"], 100);
  }

  #[tokio::test]
  async fn test_all_http_status_codes() {
    let test_cases = vec![
      (ErrorCode::InvalidInput, StatusCode::BAD_REQUEST),
      (ErrorCode::Unauthorized, StatusCode::UNAUTHORIZED),
      (ErrorCode::Forbidden, StatusCode::FORBIDDEN),
      (ErrorCode::ResourceNotFound, StatusCode::NOT_FOUND),
      (ErrorCode::Conflict, StatusCode::CONFLICT),
      (ErrorCode::RateLimited, StatusCode::TOO_MANY_REQUESTS),
      (ErrorCode::Internal, StatusCode::INTERNAL_SERVER_ERROR),
      (ErrorCode::Unavailable, StatusCode::SERVICE_UNAVAILABLE),
      (ErrorCode::Timeout, StatusCode::GATEWAY_TIMEOUT),
    ];

    for (code, expected_status) in test_cases {
      let error = Error::new(code, "test_error", "Test message");
      let response = error.into_response();
      assert_eq!(
        response.status(),
        expected_status,
        "Failed for code {:?}",
        code
      );
    }
  }

  #[tokio::test]
  async fn test_define_errors_macro_with_into() {
    define_errors! {
      ErrorCode::ResourceNotFound => [
        TestUserNotFound,
      ],
    }

    let error: Error = TestUserNotFound.into();
    assert_eq!(error.code(), ErrorCode::ResourceNotFound);
    assert_eq!(error.error_type(), "TestUserNotFound");

    let response = error.into_response();
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
  }

  #[tokio::test]
  async fn test_error_json_format() {
    let error = InvalidEmail::error_with_data(
      "Invalid email format",
      json!({"field": "email", "pattern": ".*@.*"}),
    );

    let response = error.into_response();
    let body = response.into_body().collect().await.unwrap().to_bytes();
    let json: Value = serde_json::from_slice(&body).unwrap();

    assert!(json["error"].is_object());
    assert_eq!(json["error"]["code"], "InvalidInput");
    assert_eq!(json["error"]["type"], "InvalidEmail");
    assert_eq!(json["error"]["message"], "Invalid email format");
    assert_eq!(json["error"]["data"]["field"], "email");
    assert_eq!(json["error"]["data"]["pattern"], ".*@.*");
  }
}