Skip to main content

autumn_plugin_discord/
error.rs

1//! Error types for the Discord plugin.
2//!
3//! [`Error`] is the plugin's own error. [`ErrorKind`] classifies failures so
4//! callers can match on them. [`Error::to_autumn_error`] maps each kind to an
5//! [`AutumnError`](autumn_web::AutumnError) with a sensible HTTP status.
6
7use autumn_web::AutumnError;
8use thiserror::Error;
9
10/// Classifies a [`Error`] without exposing its payload.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum ErrorKind {
14    /// The `[discord]` config is missing or invalid.
15    Config,
16    /// The interaction signature header is missing, malformed, or invalid.
17    Signature,
18    /// A JSON payload failed to parse or serialize.
19    Json,
20    /// A Discord REST call failed at the transport level.
21    Http,
22    /// The Discord API answered with a non-success status.
23    DiscordApi,
24    /// The Discord gateway connection failed.
25    Gateway,
26    /// A command name has no registered handler.
27    UnknownCommand,
28    /// The plugin runtime is not installed (the startup hook did not run).
29    MissingRuntime,
30}
31
32/// The plugin's error type.
33#[derive(Debug, Error)]
34#[non_exhaustive]
35pub enum Error {
36    /// The `[discord]` config is missing or invalid.
37    #[error("discord config error: {0}")]
38    Config(String),
39
40    /// The interaction signature is missing, malformed, or invalid.
41    #[error("discord signature error: {0}")]
42    Signature(String),
43
44    /// A JSON payload failed to parse or serialize.
45    #[error("discord json error: {0}")]
46    Json(#[from] serde_json::Error),
47
48    /// A Discord REST call failed at the transport level.
49    #[error("discord http error: {0}")]
50    Http(#[from] reqwest::Error),
51
52    /// The Discord API answered with a non-success status.
53    #[error("discord api error: status {status}: {body}")]
54    DiscordApi {
55        /// The HTTP status code Discord returned.
56        status: u16,
57        /// The response body, truncated.
58        body: String,
59    },
60
61    /// The Discord gateway connection failed.
62    #[error("discord gateway error: {0}")]
63    Gateway(String),
64
65    /// A command name has no registered handler.
66    #[error("unknown discord command: {0}")]
67    UnknownCommand(String),
68
69    /// The plugin runtime is not installed (the startup hook did not run).
70    #[error("discord runtime not installed")]
71    MissingRuntime,
72}
73
74impl Error {
75    /// Classify this error.
76    #[must_use]
77    pub const fn kind(&self) -> ErrorKind {
78        match self {
79            Self::Config(_) => ErrorKind::Config,
80            Self::Signature(_) => ErrorKind::Signature,
81            Self::Json(_) => ErrorKind::Json,
82            Self::Http(_) => ErrorKind::Http,
83            Self::DiscordApi { .. } => ErrorKind::DiscordApi,
84            Self::Gateway(_) => ErrorKind::Gateway,
85            Self::UnknownCommand(_) => ErrorKind::UnknownCommand,
86            Self::MissingRuntime => ErrorKind::MissingRuntime,
87        }
88    }
89
90    /// The HTTP status code this error maps to.
91    #[must_use]
92    pub const fn status_code(&self) -> u16 {
93        match self.kind() {
94            // Upstream failures surface as 500: AutumnError has no 502
95            // constructor, so `to_autumn_error` uses 500 for these.
96            ErrorKind::Config | ErrorKind::Http | ErrorKind::DiscordApi | ErrorKind::Gateway => 500,
97            ErrorKind::Signature => 401,
98            ErrorKind::Json | ErrorKind::UnknownCommand => 400,
99            ErrorKind::MissingRuntime => 503,
100        }
101    }
102
103    /// Convert into an [`AutumnError`](autumn_web::AutumnError).
104    ///
105    /// The mapping keeps Discord's own semantics: bad signatures are 401,
106    /// a missing runtime is 503, upstream Discord failures are 502.
107    #[must_use]
108    pub fn to_autumn_error(&self) -> AutumnError {
109        let message = self.to_string();
110        match self.kind() {
111            ErrorKind::Signature => AutumnError::unauthorized_msg(message),
112            ErrorKind::Json | ErrorKind::UnknownCommand => AutumnError::bad_request_msg(message),
113            ErrorKind::MissingRuntime => AutumnError::service_unavailable_msg(message),
114            ErrorKind::Config | ErrorKind::Http | ErrorKind::DiscordApi | ErrorKind::Gateway => {
115                AutumnError::internal_server_error_msg(message)
116            }
117        }
118    }
119}
120
121/// Convenience alias.
122pub type Result<T> = std::result::Result<T, Error>;
123
124#[cfg(test)]
125mod tests;