essence 0.3.3

Essential models and database logic for the Adapt chat platform.
Documentation
use crate::models::Permissions;
#[cfg(feature = "client")]
use serde::Deserialize;
use serde::Serialize;
#[cfg(feature = "openapi")]
use utoipa::ToSchema;

/// A type alias for a [`Result`] with the error type [`Error`].
pub type Result<T> = std::result::Result<T, Error>;

#[derive(Copy, Clone, Debug, Serialize)]
#[cfg_attr(feature = "client", derive(Deserialize))]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum MalformedBodyErrorType {
    /// Invalid content type.
    InvalidContentType,
    /// Body was invalid UTF-8.
    InvalidUtf8,
    /// Received invalid JSON body.
    InvalidJson,
}

/// An error that occurs within Adapt.
#[derive(Clone, Debug, Serialize)]
#[cfg_attr(feature = "client", derive(Deserialize))]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Error {
    /// Received a malformed JSON or MsgPack body.
    MalformedBody {
        /// Extra information about the error.
        error_type: MalformedBodyErrorType,
        /// A generalized message about the error.
        message: String,
    },
    /// You are missing the request body in an endpoint that requires it. This is commonly JSON
    /// or MsgPack.
    MissingBody {
        /// The error message.
        message: String,
    },
    /// Invalid field in the request body.
    InvalidField {
        /// The field that failed validation.
        field: String,
        /// The error message.
        message: String,
    },
    /// You are missing a required field in the request body.
    MissingField {
        /// The name of the missing field.
        field: String,
        /// The error message.
        message: String,
    },
    /// Could not resolve a plausible IP address from the request.
    MalformedIp {
        /// The error message.
        message: String,
    },
    /// The entity was not found.
    NotFound {
        /// The type of item that couldn't be found.
        entity: String,
        /// The error message.
        message: String,
    },
    /// Tried authorizing a bot account with anything but an authentication token.
    UnsupportedAuthMethod {
        /// The error message.
        message: String,
    },
    /// The request required a valid authentication token, but one of the following happened:
    ///
    /// * The token was not provided.
    /// * The token was malformed, i.e. a non-UTF-8 string.
    /// * The token does not exist or is invalid.
    InvalidToken {
        /// The error message.
        message: String,
    },
    /// Invalid login credentials were provided, i.e. an invalid password.
    InvalidCredentials {
        /// Which credential was invalid.
        what: String,
        /// The error message.
        message: String,
    },
    /// You must be a member of the guild to perform the requested action.
    NotMember {
        /// The ID of the guild you are not a member of.
        guild_id: u64,
        /// The error message.
        message: String,
    },
    /// You must be the owner of the guild to perform the requested action.
    NotOwner {
        /// The ID of the guild you are not the owner of.
        guild_id: u64,
        /// The error message.
        message: String,
    },
    /// You are too low in the role hierarchy to perform the requested action.
    RoleTooLow {
        /// The ID of the guild you are not the owner of.
        guild_id: u64,
        /// The ID of your top role. This is the role you possess with the highest position.
        /// This is `None` if you have no roles (the default role).
        top_role_id: Option<u64>,
        /// The position of your top role.
        top_role_position: u16,
        /// The desired position your top role should be in the role hierarchy.
        desired_position: u16,
        /// The error message.
        message: String,
    },
    /// You are missing the required permissions to perform the requested action.
    MissingPermissions {
        /// The ID of the guild you are missing permissions in.
        guild_id: u64,
        /// The permissions required to perform the requested action.
        permissions: Permissions,
        /// The error message.
        message: String,
    },
    /// You are trying to delete a managed role.
    RoleIsManaged {
        /// The ID of the guild the role is in.
        guild_id: u64,
        /// The ID of the role that is managed.
        role_id: u64,
        /// The error message.
        message: String,
    },
    /// Something was already taken, e.g. a username or email.
    AlreadyTaken {
        /// What was already taken.
        what: String,
        /// The error message.
        message: String,
    },
    /// You are sending requests too quickly are you are being rate limited.
    Ratelimited {
        /// How long you should wait before sending another request, in whole seconds.
        retry_after: f32,
        /// The IP address that is being rate limited.
        ip: String,
        /// The ratelimited message.
        message: String,
    },
    /// Internal server error occured, this is likely a bug.
    InternalError {
        /// What caused the error. `None` if unknown.
        what: Option<String>,
        /// The error message.
        message: String,
        /// A debug version of the error, or `None` if there is no debug version.
        debug: Option<String>,
    },
}

impl Error {
    /// The HTTP status code associated with this error. If this error is not sent over HTTP,
    /// this will be `None`.
    #[must_use]
    pub const fn http_status_code(&self) -> Option<u16> {
        Some(match self {
            Self::MalformedBody { .. }
            | Self::MissingBody { .. }
            | Self::InvalidField { .. }
            | Self::MissingField { .. }
            | Self::MalformedIp { .. }
            | Self::UnsupportedAuthMethod { .. } => 400,
            Self::InvalidToken { .. } | Self::InvalidCredentials { .. } => 401,
            Self::NotMember { .. }
            | Self::NotOwner { .. }
            | Self::MissingPermissions { .. }
            | Self::RoleTooLow { .. }
            | Self::RoleIsManaged { .. } => 403,
            Self::NotFound { .. } => 404,
            Self::AlreadyTaken { .. } => 409,
            Self::Ratelimited { .. } => 429,
            Self::InternalError { .. } => 500,
        })
    }
}

#[cfg(feature = "db")]
impl From<sqlx::Error> for Error {
    fn from(e: sqlx::Error) -> Self {
        Self::InternalError {
            what: Some("database".into()),
            message: e.to_string(),
            debug: Some(format!("{e:?}")),
        }
    }
}

#[cfg(feature = "auth")]
impl From<argon2_async::Error> for Error {
    fn from(e: argon2_async::Error) -> Self {
        Self::InternalError {
            what: Some("hasher".into()),
            message: e.to_string(),
            debug: Some(format!("{e:?}")),
        }
    }
}

/// An extension trait for [`Option`] that adds [`NotFoundExt::ok_or_not_found`].
pub trait NotFoundExt<T> {
    /// Converts an [`Option`] to a [`Result`] with [`Error::NotFound`] if it is [`None`].
    ///
    /// # Example
    /// ```no_run
    /// use essence::error::NotFoundExt;
    ///
    /// assert_eq!(Some(5).ok_or_not_found("user", "user not found"), Ok(5));
    /// ```
    fn ok_or_not_found(self, entity: impl ToString, message: impl ToString) -> Result<T>;
}

impl<T> NotFoundExt<T> for Option<T> {
    #[inline]
    fn ok_or_not_found(self, entity: impl ToString, message: impl ToString) -> Result<T> {
        self.ok_or_else(|| Error::NotFound {
            entity: entity.to_string(),
            message: message.to_string(),
        })
    }
}