1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! An Error to respond a HTTP status.

use sfr_types as st;

use crate::SlashCommandResponse;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::BoxError;
use axum::Json;
use serde_json::json;

/// The type to respond a HTTP status.
#[derive(Debug)]
pub enum ResponseError {
    /// The Slack request has unexpected headers.
    InvalidHeader(String),

    /// Failed to read the request body.
    ReadBody,

    /// Failed to deserialize the request body.
    DeserializeBody(BoxError),

    /// The Slack Verification Token is not matched.
    InvalidToken,

    /// The Method is not `POST`.
    ///
    /// > A payload is sent via an HTTP POST request to your app.
    ///
    /// <https://api.slack.com/interactivity/slash-commands#getting_started>
    MethodNotAllowed,

    /// Occurred an error in the user source.
    InternalServerError(Box<dyn std::error::Error + 'static + Send + Sync>),

    /// Occurred an unexpected state.
    Unknown,

    /// The custom error from the user source.
    Custom(StatusCode, String),

    /// The custom response for fast-return.
    CustomResponse(SlashCommandResponse),
}

impl ResponseError {
    /// Creates [`ResponseError::InternalServerError`].
    pub fn internal_server_error(inner: impl std::error::Error + 'static + Send + Sync) -> Self {
        Self::InternalServerError(Box::new(inner))
    }

    /// Creates a custom response.
    pub fn custom_response<T>(resp: T) -> Self
    where
        T: Into<SlashCommandResponse>,
    {
        Self::CustomResponse(resp.into())
    }

    /// Creates a custom error.
    pub fn custom<T1, T2>(status: T1, message: T2) -> Result<Self, st::Error>
    where
        T1: TryInto<StatusCode>,
        <T1 as TryInto<StatusCode>>::Error: std::error::Error + 'static + Send + Sync,
        T2: Into<String>,
    {
        let status = status
            .try_into()
            .map_err(|e| st::ServerError::InvalidStatusCode(Box::new(e)))?;
        Ok(Self::Custom(status, message.into()))
    }

    /// Maps the tuple of the HTTP [`StatusCode`] and the error message from [`ResponseError`].
    fn map(&self) -> (StatusCode, &str) {
        match self {
            Self::InvalidHeader(_) => (StatusCode::BAD_REQUEST, "Invalid header"),
            Self::ReadBody => (StatusCode::BAD_REQUEST, "Failed to read body"),
            Self::DeserializeBody(_) => (StatusCode::BAD_REQUEST, "Failed to deserialize"),
            Self::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid body"),

            Self::MethodNotAllowed => (StatusCode::METHOD_NOT_ALLOWED, "method not allowed"),

            Self::InternalServerError(_) | Self::Unknown => {
                (StatusCode::INTERNAL_SERVER_ERROR, "Unknown error")
            }

            Self::Custom(code, message) => (*code, message.as_str()),

            Self::CustomResponse(_) => unreachable!(),
        }
    }
}

impl IntoResponse for ResponseError {
    fn into_response(self) -> Response {
        if let Self::CustomResponse(resp) = self {
            return resp.into_response();
        }

        tracing::info!("response error: {self:?}");

        let (status, error_message) = self.map();
        let body = Json(json!({
            "error": error_message,
        }));
        (status, body).into_response()
    }
}

impl std::fmt::Display for ResponseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        let (status, error_message) = self.map();
        write!(f, "{status}: {error_message}")
    }
}

impl std::error::Error for ResponseError {}