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
119
120
121
122
123
124
125
//! Error types for the Discord plugin.
//!
//! [`Error`] is the plugin's own error. [`ErrorKind`] classifies failures so
//! callers can match on them. [`Error::to_autumn_error`] maps each kind to an
//! [`AutumnError`](autumn_web::AutumnError) with a sensible HTTP status.
use autumn_web::AutumnError;
use thiserror::Error;
/// Classifies a [`Error`] without exposing its payload.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
/// The `[discord]` config is missing or invalid.
Config,
/// The interaction signature header is missing, malformed, or invalid.
Signature,
/// A JSON payload failed to parse or serialize.
Json,
/// A Discord REST call failed at the transport level.
Http,
/// The Discord API answered with a non-success status.
DiscordApi,
/// The Discord gateway connection failed.
Gateway,
/// A command name has no registered handler.
UnknownCommand,
/// The plugin runtime is not installed (the startup hook did not run).
MissingRuntime,
}
/// The plugin's error type.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
/// The `[discord]` config is missing or invalid.
#[error("discord config error: {0}")]
Config(String),
/// The interaction signature is missing, malformed, or invalid.
#[error("discord signature error: {0}")]
Signature(String),
/// A JSON payload failed to parse or serialize.
#[error("discord json error: {0}")]
Json(#[from] serde_json::Error),
/// A Discord REST call failed at the transport level.
#[error("discord http error: {0}")]
Http(#[from] reqwest::Error),
/// The Discord API answered with a non-success status.
#[error("discord api error: status {status}: {body}")]
DiscordApi {
/// The HTTP status code Discord returned.
status: u16,
/// The response body, truncated.
body: String,
},
/// The Discord gateway connection failed.
#[error("discord gateway error: {0}")]
Gateway(String),
/// A command name has no registered handler.
#[error("unknown discord command: {0}")]
UnknownCommand(String),
/// The plugin runtime is not installed (the startup hook did not run).
#[error("discord runtime not installed")]
MissingRuntime,
}
impl Error {
/// Classify this error.
#[must_use]
pub const fn kind(&self) -> ErrorKind {
match self {
Self::Config(_) => ErrorKind::Config,
Self::Signature(_) => ErrorKind::Signature,
Self::Json(_) => ErrorKind::Json,
Self::Http(_) => ErrorKind::Http,
Self::DiscordApi { .. } => ErrorKind::DiscordApi,
Self::Gateway(_) => ErrorKind::Gateway,
Self::UnknownCommand(_) => ErrorKind::UnknownCommand,
Self::MissingRuntime => ErrorKind::MissingRuntime,
}
}
/// The HTTP status code this error maps to.
#[must_use]
pub const fn status_code(&self) -> u16 {
match self.kind() {
// Upstream failures surface as 500: AutumnError has no 502
// constructor, so `to_autumn_error` uses 500 for these.
ErrorKind::Config | ErrorKind::Http | ErrorKind::DiscordApi | ErrorKind::Gateway => 500,
ErrorKind::Signature => 401,
ErrorKind::Json | ErrorKind::UnknownCommand => 400,
ErrorKind::MissingRuntime => 503,
}
}
/// Convert into an [`AutumnError`](autumn_web::AutumnError).
///
/// The mapping keeps Discord's own semantics: bad signatures are 401,
/// a missing runtime is 503, upstream Discord failures are 502.
#[must_use]
pub fn to_autumn_error(&self) -> AutumnError {
let message = self.to_string();
match self.kind() {
ErrorKind::Signature => AutumnError::unauthorized_msg(message),
ErrorKind::Json | ErrorKind::UnknownCommand => AutumnError::bad_request_msg(message),
ErrorKind::MissingRuntime => AutumnError::service_unavailable_msg(message),
ErrorKind::Config | ErrorKind::Http | ErrorKind::DiscordApi | ErrorKind::Gateway => {
AutumnError::internal_server_error_msg(message)
}
}
}
}
/// Convenience alias.
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests;