defter 0.7.4

Self-hosted Zotero sync and file storage server, with metadata in SQLite and attachments in S3.
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("not found: {0}")]
    NotFound(String),
    #[error("bad request: {0}")]
    BadRequest(String),

    #[error("{0}")]
    PreconditionRequired(String),
    #[error("request body too large")]
    TooLarge,
    #[error("upstream error: {0}")]
    Upstream(String),
    #[error("database error: {0}")]
    Database(#[from] rusqlite::Error),
    #[error("object storage error: {0}")]
    Storage(#[from] object_store::Error),
    #[error("internal error: {0}")]
    Internal(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let status = match &self {
            AppError::NotFound(_) => StatusCode::NOT_FOUND,
            AppError::BadRequest(_) => StatusCode::BAD_REQUEST,

            AppError::PreconditionRequired(_) => StatusCode::PRECONDITION_REQUIRED,
            AppError::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
            AppError::Upstream(_) => StatusCode::BAD_GATEWAY,
            AppError::Database(_) | AppError::Storage(_) | AppError::Internal(_) => {
                StatusCode::INTERNAL_SERVER_ERROR
            }
        };
        if status.is_server_error() {
            tracing::error!(error = %self, "request failed");
            // Internal details stay in the log.
            return (status, "internal server error").into_response();
        }
        (status, self.to_string()).into_response()
    }
}