lambda-forge 0.1.7

An opinionated API Framework for building AWS Lambda HTTP endpoints.
Documentation
use lambda_http::{Body, Response, http::StatusCode, tracing::error};
use std::fmt;

use crate::{ApiResponseBody, EndpointMetadata};

#[derive(Debug)]
pub enum Error {
    MissingLambdaContext,
    InvalidEnvironmentConfig(&'static str),
    #[cfg(feature = "db")]
    DatabaseFailure(sqlx::Error),

    BadRequest(String),
    ServerError,
    Unauthorized,
    RateLimit(String),
    Conflict(String),
    // TODO: Support more generic HTTP errors
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::MissingLambdaContext => {
                write!(f, "`EndpointMetadata` is missing the AWS Lambda Context")
            }
            Error::InvalidEnvironmentConfig(err_msg) => {
                write!(f, "Invalid Environment Config: {err_msg}")
            }
            Error::BadRequest(msg) => write!(f, "Bad Request: {msg}"),
            Error::ServerError => write!(f, "Server Error"),
            Error::Unauthorized => write!(f, "Unauthorized"),
            Error::RateLimit(msg) => write!(f, "Rate Limit Exceeded: {msg}"),
            Error::Conflict(msg) => write!(f, "Conflict: {msg}"),

            #[cfg(feature = "db")]
            Error::DatabaseFailure(err) => {
                write!(f, "Database Failure: {err}")
            }
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            #[cfg(feature = "db")]
            Error::DatabaseFailure(e) => Some(e),

            _ => None,
        }
    }
}

impl Error {
    fn http_status(&self) -> StatusCode {
        match self {
            Error::Unauthorized => StatusCode::UNAUTHORIZED,
            Error::RateLimit(_) => StatusCode::TOO_MANY_REQUESTS,
            Error::BadRequest(_) => StatusCode::BAD_REQUEST,
            Error::Conflict(_) => StatusCode::CONFLICT,

            Error::ServerError
            | Error::MissingLambdaContext
            | Error::InvalidEnvironmentConfig(_) => StatusCode::INTERNAL_SERVER_ERROR,

            #[cfg(feature = "db")]
            Error::DatabaseFailure(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }

    fn message_for_client(&self) -> String {
        let server_error_message =
            "We ran into issues processing your request, please try again later.".to_owned();
        match self {
            Error::Unauthorized => "Invalid authorization for requested resource.".to_owned(),
            Error::RateLimit(msg) => msg.to_owned(),
            Error::BadRequest(msg) => msg.to_owned(),
            Error::Conflict(msg) => msg.to_owned(),
            Error::ServerError
            | Error::MissingLambdaContext
            | Error::InvalidEnvironmentConfig(_) => server_error_message,

            #[cfg(feature = "db")]
            Error::DatabaseFailure(_) => server_error_message,
        }
    }

    /// Build a lambda_http::Response with your envelope and proper status.
    pub fn into_lambda_response(self, endpoint: EndpointMetadata) -> Response<Body> {
        error!("Converting error to lambda response: {self:?}");

        let status = self.http_status();
        let body = ApiResponseBody::<serde_json::Value> {
            was_successful: false,
            data: None,
            message: Some(self.message_for_client()),
        };

        Response::builder()
            .status(status)
            .header("content-type", endpoint.response_content_type)
            .body(Body::from(serde_json::to_string(&body).unwrap()))
            .unwrap()
    }

    /// Build a lambda_http::Response with your envelope and proper status.
    ///
    /// NOTE: This version has no endpoint context, if you have context
    /// available then prefer the `into_lambda_response` function instead.
    pub fn into_lambda_response_no_ctx(self) -> Response<Body> {
        error!("Converting error to lambda response: {self:?}");

        let status = self.http_status();
        let body = ApiResponseBody::<serde_json::Value> {
            was_successful: false,
            data: None,
            message: Some(self.message_for_client().to_string()),
        };

        Response::builder()
            .status(status)
            .header("content-type", "application/json")
            .body(Body::from(serde_json::to_string(&body).unwrap()))
            .unwrap()
    }
}

#[cfg(feature = "db")]
impl From<sqlx::Error> for Error {
    fn from(e: sqlx::Error) -> Self {
        Error::DatabaseFailure(e)
    }
}