subliminal 0.0.4

Base crate for subliminal microservices project
Documentation
use std::{error::Error, fmt::Display};

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::{json, Value};
use tonic::Status;

/// Error types related to a particular service (not the API itself).
#[derive(Debug)]
pub enum ServiceErrors {
    // When a service indicates a resource is not found.
    NotFound(Status),

    // When a service is unavailable.
    ServiceUnavailable(Status),

    // When a service returns an internal error.
    ServiceInternal(Status),
}

// Implement From<tonic::Status> so that we can convert a tonic::Status into a ServiceErrors variant.
// This is useful for translating service errors into eventual HTTP responses.
// We need a separate error enum due to the orphan rule.
impl From<tonic::Status> for ServiceErrors {
    fn from(status: tonic::Status) -> Self {
        match status.code() {
            tonic::Code::NotFound => ServiceErrors::NotFound(status),
            tonic::Code::Unavailable => ServiceErrors::ServiceUnavailable(status),
            _ => ServiceErrors::ServiceInternal(status),
        }
    }
}

/// This happens when a client is unable to connect to a service.
impl From<tonic::transport::Error> for ServiceErrors {
    fn from(error: tonic::transport::Error) -> Self {
        ServiceErrors::ServiceUnavailable(Status::new(tonic::Code::Unavailable, error.to_string()))
    }
}

impl Error for ServiceErrors {}
impl Display for ServiceErrors {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ServiceErrors::NotFound(e) => {
                write!(f, "{}", e.message())
            }
            ServiceErrors::ServiceUnavailable(e) => {
                write!(f, "{}", e.message())
            }
            ServiceErrors::ServiceInternal(e) => {
                write!(f, "{}", e.message())
            }
        }
    }
}

/// Implement IntoResponse for HttpResponse so that we can return it from an axum handler.
/// This is primarily used for outside services that the API proxies requests to, whereas the API itself
/// returns HttpResponses directly.
impl IntoResponse for ServiceErrors {
    fn into_response(self) -> Response {
        match self {
            // When this error is encountered, it means a service was unable to find a resource.
            // There are cases where we want to override this behavior, such as when we want to return
            // a 503 status code instead of a 404 when trying to get a service location from the registry.
            ServiceErrors::NotFound(e) => {
                (StatusCode::NOT_FOUND, error_json(e.message().to_string()))
            }
            // When this error is encountered, it means we were unable to connect to a particular service.
            // Instead of returning a 503 (indicating issues with the API itself), we return a 504 to indicate
            // there is an issue with proxied services.
            ServiceErrors::ServiceUnavailable(e) => (
                StatusCode::GATEWAY_TIMEOUT,
                error_json(e.message().to_string()),
            ),
            // When this error is encountered, it means a service (not the API) encountered an internal error.
            ServiceErrors::ServiceInternal(e) => {
                (StatusCode::BAD_GATEWAY, error_json(e.message().to_string()))
            }
        }
        .into_response()
    }
}

/// Quick helper to convert an error string into a JSON object.
fn error_json(error: String) -> Json<Value> {
    Json(json!({ "error": error }))
}