use crate::ValidationErrors;
#[cfg(feature = "reqwest")]
use crate::helpers::reqwest::ReqwestResponseError;
use crate::results::AppResult;
use http::StatusCode;
use std::borrow::Cow;
use std::env::VarError;
use std::fmt::{Debug, Display, Formatter};
use thiserror::Error;
use tracing::{error, info, warn};
#[derive(Error, Debug, Clone)]
pub enum AppMessage {
Success(String),
Redirect(String),
Invalid(String),
Unauthorized(String),
Forbidden(String),
NotFound(String),
Conflict(String),
UnprocessableEntity(String),
ValidationError(String, ValidationErrors),
InternalServerError(String),
ErrorMessage(String, StatusCode),
MissingEnvironmentVariable(String, VarError),
#[cfg(feature = "reqwest")]
ReqwestResponseError(ReqwestResponseError),
}
impl Display for AppMessage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message())
}
}
impl AppMessage {
pub fn success(msg: impl Into<String>) -> Self {
AppMessage::Success(msg.into())
}
pub fn redirect(url: impl Into<String>) -> Self {
AppMessage::Redirect(url.into())
}
pub fn invalid(msg: impl Into<String>) -> Self {
AppMessage::Invalid(msg.into())
}
pub fn unauthorized(msg: impl Into<String>) -> Self {
AppMessage::Unauthorized(msg.into())
}
pub fn forbidden(msg: impl Into<String>) -> Self {
AppMessage::Forbidden(msg.into())
}
pub fn not_found(msg: impl Into<String>) -> Self {
AppMessage::NotFound(msg.into())
}
pub fn conflict(msg: impl Into<String>) -> Self {
AppMessage::Conflict(msg.into())
}
pub fn unprocessable_entity(msg: impl Into<String>) -> Self {
AppMessage::UnprocessableEntity(msg.into())
}
pub fn validation_error(msg: impl Into<String>, errors: impl Into<ValidationErrors>) -> Self {
AppMessage::ValidationError(msg.into(), errors.into())
}
pub fn internal_server_error(msg: impl Into<String>) -> Self {
AppMessage::InternalServerError(msg.into())
}
pub fn error_message(msg: impl Into<String>, status: StatusCode) -> Self {
AppMessage::ErrorMessage(msg.into(), status)
}
pub fn missing_environment_variable(name: impl Into<String>, error: VarError) -> Self {
AppMessage::MissingEnvironmentVariable(name.into(), error)
}
#[cfg(feature = "reqwest")]
pub fn reqwest_response_error(err: ReqwestResponseError) -> Self {
AppMessage::ReqwestResponseError(err)
}
pub fn status_code(&self) -> StatusCode {
match self {
AppMessage::Success(_) => StatusCode::OK,
AppMessage::Redirect(_) => StatusCode::FOUND,
AppMessage::Invalid(_) => StatusCode::BAD_REQUEST,
AppMessage::Unauthorized(_) => StatusCode::UNAUTHORIZED,
AppMessage::Forbidden(_) => StatusCode::FORBIDDEN,
AppMessage::NotFound(_) => StatusCode::NOT_FOUND,
AppMessage::Conflict(_) => StatusCode::CONFLICT,
AppMessage::UnprocessableEntity(_) | AppMessage::ValidationError(_, _) => {
StatusCode::UNPROCESSABLE_ENTITY
}
AppMessage::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppMessage::MissingEnvironmentVariable(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
AppMessage::ErrorMessage(_, status) => *status,
#[cfg(feature = "reqwest")]
AppMessage::ReqwestResponseError(err) => *err.code(),
}
}
pub fn message(&self) -> Cow<'_, str> {
match self {
AppMessage::Success(msg)
| AppMessage::Invalid(msg)
| AppMessage::Redirect(msg)
| AppMessage::Unauthorized(msg)
| AppMessage::Forbidden(msg)
| AppMessage::NotFound(msg)
| AppMessage::Conflict(msg)
| AppMessage::UnprocessableEntity(msg)
| AppMessage::InternalServerError(msg) => Cow::from(msg),
AppMessage::ValidationError(msg, _) => Cow::from(msg),
AppMessage::ErrorMessage(msg, _) => Cow::from(msg),
AppMessage::MissingEnvironmentVariable(name, e) => {
Cow::from(format!("Missing environment variable '{name}': {e}"))
}
#[cfg(feature = "reqwest")]
AppMessage::ReqwestResponseError(err) => Cow::from(err.body().to_string()),
}
}
pub fn validation_errors(&self) -> Option<&ValidationErrors> {
match self {
AppMessage::ValidationError(_, errors) => Some(errors),
_ => None,
}
}
pub fn kind_name(&self) -> &'static str {
match self {
AppMessage::Success(_) => "success",
AppMessage::Redirect(_) => "redirect",
AppMessage::Invalid(_) => "invalid",
AppMessage::Unauthorized(_) => "unauthorized",
AppMessage::Forbidden(_) => "forbidden",
AppMessage::NotFound(_) => "not_found",
AppMessage::Conflict(_) => "conflict",
AppMessage::UnprocessableEntity(_) => "unprocessable_entity",
AppMessage::ValidationError(_, _) => "validation_error",
AppMessage::InternalServerError(_) => "internal_server_error",
AppMessage::MissingEnvironmentVariable(_, _) => "missing_environment_variable",
AppMessage::ErrorMessage(_, _) => "error_message",
#[cfg(feature = "reqwest")]
AppMessage::ReqwestResponseError(_) => "reqwest_response_error",
}
}
pub fn is_success(&self) -> bool {
self.status_code().is_success()
}
pub fn is_redirect(&self) -> bool {
self.status_code().is_redirection()
}
pub fn is_client_error(&self) -> bool {
self.status_code().is_client_error()
}
pub fn is_server_error(&self) -> bool {
self.status_code().is_server_error()
}
pub fn is_error(&self) -> bool {
self.is_client_error() || self.is_server_error()
}
pub fn log(&self) {
let kind = self.kind_name();
let status = self.status_code().as_u16();
let msg = self.message();
if self.is_success() || self.is_redirect() {
info!(kind, status, "{}", msg);
} else if self.is_client_error() {
warn!(kind, status, "{}", msg);
} else {
error!(kind, status, "{}", msg);
}
}
pub fn into_anyhow(self) -> anyhow::Error {
anyhow::Error::from(self)
}
pub fn into_result<T>(self) -> AppResult<T> {
Err(self.into_anyhow())
}
}
impl From<crate::Error> for AppMessage {
fn from(value: anyhow::Error) -> Self {
value.downcast::<AppMessage>().unwrap_or_else(|e| {
error!("AppMessage downcast failed, wrapping as InternalServerError: {e}");
AppMessage::InternalServerError(e.to_string())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_success() {
let msg = AppMessage::success("User created");
assert_eq!(msg.status_code(), StatusCode::OK);
assert_eq!(msg.message(), "User created");
assert!(msg.is_success());
assert!(!msg.is_error());
assert!(!msg.is_redirect());
assert_eq!(msg.kind_name(), "success");
}
#[test]
fn test_redirect_is_not_an_error() {
let msg = AppMessage::redirect("https://foxtive.com");
assert_eq!(msg.status_code(), StatusCode::FOUND);
assert!(msg.is_redirect());
assert!(!msg.is_error()); assert!(!msg.is_success());
assert_eq!(msg.kind_name(), "redirect");
}
#[test]
fn test_invalid() {
let msg = AppMessage::invalid("Invalid pin");
assert_eq!(msg.status_code(), StatusCode::BAD_REQUEST);
assert!(msg.is_client_error());
assert!(msg.is_error());
assert_eq!(msg.kind_name(), "invalid");
}
#[test]
fn test_unauthorized() {
let msg = AppMessage::unauthorized("Invalid auth token");
assert_eq!(msg.status_code(), StatusCode::UNAUTHORIZED);
assert!(msg.is_client_error());
assert_eq!(msg.message(), "Invalid auth token");
}
#[test]
fn test_forbidden() {
let msg = AppMessage::forbidden("Insufficient permissions");
assert_eq!(msg.status_code(), StatusCode::FORBIDDEN);
assert!(msg.is_client_error());
}
#[test]
fn test_not_found() {
let msg = AppMessage::not_found("Could not locate wallet");
assert_eq!(msg.status_code(), StatusCode::NOT_FOUND);
assert!(msg.is_client_error());
}
#[test]
fn test_conflict() {
let msg = AppMessage::conflict("Email already in use");
assert_eq!(msg.status_code(), StatusCode::CONFLICT);
assert!(msg.is_client_error());
assert_eq!(msg.kind_name(), "conflict");
}
#[test]
fn test_unprocessable_entity() {
let msg = AppMessage::unprocessable_entity("Invalid payload");
assert_eq!(msg.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
assert!(msg.is_client_error());
}
#[test]
fn test_validation_error() {
let mut errors = ValidationErrors::new();
errors.insert("email".into(), vec!["is required".into()]);
errors.insert("name".into(), vec!["is too short".into()]);
let msg = AppMessage::validation_error("Validation failed", errors.clone());
assert_eq!(msg.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
assert!(msg.is_client_error());
assert_eq!(msg.message(), "Validation failed");
assert_eq!(msg.kind_name(), "validation_error");
let returned = msg.validation_errors().unwrap();
assert_eq!(returned["email"], vec!["is required"]);
assert_eq!(returned["name"], vec!["is too short"]);
}
#[test]
fn test_validation_errors_none_for_other_variants() {
assert!(AppMessage::not_found("x").validation_errors().is_none());
assert!(AppMessage::success("x").validation_errors().is_none());
}
#[test]
fn test_internal_server_error() {
let msg = AppMessage::internal_server_error("Y2k ever!");
assert_eq!(msg.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
assert!(msg.is_server_error());
assert!(msg.is_error());
}
#[test]
fn test_error_message_explicit_status() {
let msg = AppMessage::error_message("Y2k huh?", StatusCode::BAD_REQUEST);
assert_eq!(msg.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(msg.message(), "Y2k huh?");
}
#[test]
fn test_missing_environment_variable() {
let msg =
AppMessage::missing_environment_variable("DATABASE_URL", env::VarError::NotPresent);
assert_eq!(msg.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
assert!(msg.is_server_error());
assert_eq!(
msg.message(),
"Missing environment variable 'DATABASE_URL': environment variable not found"
);
}
#[test]
fn test_into_result() {
let result =
AppMessage::error_message("Y2k huh?", StatusCode::BAD_REQUEST).into_result::<()>();
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Y2k huh?");
}
#[test]
fn test_into_anyhow() {
let error = AppMessage::error_message("Y2k huh?", StatusCode::BAD_REQUEST).into_anyhow();
assert_eq!(error.to_string(), "Y2k huh?");
}
#[test]
fn test_kind_name_coverage() {
assert_eq!(AppMessage::success("").kind_name(), "success");
assert_eq!(AppMessage::redirect("").kind_name(), "redirect");
assert_eq!(AppMessage::invalid("").kind_name(), "invalid");
assert_eq!(AppMessage::unauthorized("").kind_name(), "unauthorized");
assert_eq!(AppMessage::forbidden("").kind_name(), "forbidden");
assert_eq!(AppMessage::not_found("").kind_name(), "not_found");
assert_eq!(AppMessage::conflict("").kind_name(), "conflict");
assert_eq!(
AppMessage::unprocessable_entity("").kind_name(),
"unprocessable_entity"
);
assert_eq!(
AppMessage::validation_error("", ValidationErrors::new()).kind_name(),
"validation_error"
);
assert_eq!(
AppMessage::internal_server_error("").kind_name(),
"internal_server_error"
);
assert_eq!(
AppMessage::missing_environment_variable("X", env::VarError::NotPresent).kind_name(),
"missing_environment_variable"
);
}
#[cfg(feature = "reqwest")]
#[test]
fn test_reqwest_response_error() {
let msg = AppMessage::reqwest_response_error(ReqwestResponseError::create(
StatusCode::BAD_REQUEST,
"Field 'user_id' is required".to_string(),
));
assert_eq!(msg.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(msg.message(), "Field 'user_id' is required");
assert!(msg.is_client_error());
}
}