use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
BadRequest,
Unauthenticated,
Unauthorized,
NotFound,
MethodNotAllowed,
Conflict,
PayloadTooLarge,
Validation,
TooManyRequests,
Internal,
ServiceUnavailable,
Status(u16),
}
impl ErrorKind {
pub fn status(self) -> u16 {
match self {
ErrorKind::BadRequest => 400,
ErrorKind::Unauthenticated => 401,
ErrorKind::Unauthorized => 403,
ErrorKind::NotFound => 404,
ErrorKind::MethodNotAllowed => 405,
ErrorKind::Conflict => 409,
ErrorKind::PayloadTooLarge => 413,
ErrorKind::Validation => 422,
ErrorKind::TooManyRequests => 429,
ErrorKind::Internal => 500,
ErrorKind::ServiceUnavailable => 503,
ErrorKind::Status(code) => code,
}
}
pub fn is_server_error(self) -> bool {
self.status() >= 500
}
}
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
message: String,
details: Option<serde_json::Value>,
source: Option<anyhow::Error>,
}
impl Error {
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
Self { kind, message: message.into(), details: None, source: None }
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Internal, message)
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(ErrorKind::BadRequest, message)
}
pub fn unauthenticated(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Unauthenticated, message)
}
pub fn unauthorized(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Unauthorized, message)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(ErrorKind::NotFound, message)
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Conflict, message)
}
pub fn request_timeout(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Status(408), message)
}
pub fn too_many_requests(message: impl Into<String>) -> Self {
Self::new(ErrorKind::TooManyRequests, message)
}
pub fn service_unavailable(message: impl Into<String>) -> Self {
Self::new(ErrorKind::ServiceUnavailable, message)
}
pub fn validation(details: serde_json::Value) -> Self {
Self::new(ErrorKind::Validation, "The given data was invalid.").with_details(details)
}
pub fn with_details(mut self, details: serde_json::Value) -> Self {
self.details = Some(details);
self
}
pub fn with_source(mut self, source: impl Into<anyhow::Error>) -> Self {
self.source = Some(source.into());
self
}
pub fn with_kind(mut self, kind: ErrorKind) -> Self {
self.kind = kind;
self
}
pub fn kind(&self) -> ErrorKind {
self.kind
}
pub fn status(&self) -> u16 {
self.kind.status()
}
pub fn message(&self) -> &str {
&self.message
}
pub fn details(&self) -> Option<&serde_json::Value> {
self.details.as_ref()
}
pub fn source_error(&self) -> Option<&anyhow::Error> {
self.source.as_ref()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)?;
if let Some(source) = &self.source {
write!(f, ": {source}")?;
}
Ok(())
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
}
}
impl From<anyhow::Error> for Error {
fn from(source: anyhow::Error) -> Self {
Self::internal(source.to_string()).with_source(source)
}
}
impl From<std::io::Error> for Error {
fn from(source: std::io::Error) -> Self {
let kind = match source.kind() {
std::io::ErrorKind::NotFound => ErrorKind::NotFound,
std::io::ErrorKind::PermissionDenied => ErrorKind::Unauthorized,
_ => ErrorKind::Internal,
};
Self::new(kind, source.to_string()).with_source(anyhow::Error::new(source))
}
}
impl From<serde_json::Error> for Error {
fn from(source: serde_json::Error) -> Self {
Self::bad_request(format!("malformed JSON: {source}"))
.with_source(anyhow::Error::new(source))
}
}
impl From<std::fmt::Error> for Error {
fn from(source: std::fmt::Error) -> Self {
Self::internal(source.to_string())
}
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub trait Context<T> {
fn context(self, context: impl fmt::Display) -> Result<T>;
fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T>;
}
impl<T, E> Context<T> for Result<T, E>
where
E: Into<Error>,
{
fn context(self, context: impl fmt::Display) -> Result<T> {
self.map_err(|e| {
let e = e.into();
let kind = e.kind();
Error::new(kind, format!("{context}: {e}"))
})
}
fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T> {
self.map_err(|e| {
let e = e.into();
let kind = e.kind();
Error::new(kind, format!("{}: {e}", context()))
})
}
}
impl<T> Context<T> for Option<T> {
fn context(self, context: impl fmt::Display) -> Result<T> {
self.ok_or_else(|| Error::internal(context.to_string()))
}
fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T> {
self.ok_or_else(|| Error::internal(context().to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kinds_map_to_statuses() {
assert_eq!(ErrorKind::Validation.status(), 422);
assert_eq!(ErrorKind::Unauthenticated.status(), 401);
assert_eq!(ErrorKind::Status(418).status(), 418);
assert!(ErrorKind::Internal.is_server_error());
assert!(!ErrorKind::NotFound.is_server_error());
}
#[test]
fn context_preserves_the_kind() {
let err: Result<()> = Err(Error::not_found("no such post"));
let err = err.context("loading the post").unwrap_err();
assert_eq!(err.kind(), ErrorKind::NotFound);
assert_eq!(err.message(), "loading the post: no such post");
}
#[test]
fn validation_errors_carry_their_details() {
let err = Error::validation(serde_json::json!({ "email": ["required"] }));
assert_eq!(err.status(), 422);
assert!(err.details().is_some());
}
#[test]
fn anyhow_errors_arrive_as_internal() {
let err: Error = anyhow::anyhow!("connection reset").into();
assert_eq!(err.status(), 500);
assert!(err.source_error().is_some());
}
}