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
126
127
128
129
130
131
//! Everything that can go wrong, in one enum.
use core::fmt;
use shep_client::{ConnectError, RequestError};
// Declared whole now because it is this crate's own interface, and every
// later module that touches the shepherd depends on it existing already
// rather than growing its own ad hoc error type.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
/// The first connection, or a reconnect the supervisor gave up on.
///
/// Live already, through `impl From<ConnectError> for Error`, even
/// though nothing in `main` calls it yet: dead-code analysis counts a
/// variant constructed inside a trait impl as constructed, whether or
/// not the impl itself is ever invoked.
Connect(ConnectError),
/// A request the shepherd refused or could not answer.
///
/// Live for the same reason as `Connect`, through
/// `impl From<RequestError> for Error`.
Request(RequestError),
/// The shepherd answered, with something else. Names both sides,
/// because "unexpected response" alone sends the reader to the wrong
/// end of the wire.
///
/// Constructed in `shepherd::unexpected`, the one place this crate
/// reads a `Response`.
Unexpected { asked: String, got: String },
/// This dog's own section of `dogs.toml` did not parse, or a value in
/// it is outside what this dog accepts.
///
/// Carries the problem and not the section name, because the section
/// is whatever name this dog was adopted under: `[discord]` only by
/// default. Whoever prints this knows that name and says it;
/// `crate::run::config_failed_message` is the one place that does.
Config(String),
/// Discord itself refused or could not answer a request a command
/// made once the gateway came up: a broken token, a permission the
/// guild never granted the bot, or the request otherwise failing.
///
/// Nothing needed this before a `Command` existed with a reason to
/// call Discord back: `Connect` and `Request` above are the shepherd
/// side of this dog, and this is the same wrapping for the other
/// side, through `impl From<serenity::Error> for Error`.
///
/// Boxed rather than inline: `serenity::Error` is over 100 bytes on
/// its own, and an unboxed variant that size would make every `Result<
/// _, Error>` in this crate pay for the biggest variant on every
/// return, `Ok` included, whether or not it ever carries a Discord
/// error.
Discord(Box<serenity::Error>),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Connect(err) => write!(f, "cannot reach the shepherd: {err}"),
Self::Request(err) => write!(f, "the shepherd refused a request: {err}"),
Self::Unexpected { asked, got } => {
write!(f, "asked the shepherd for {asked} and got {got}")
}
Self::Config(message) => write!(f, "{message}"),
Self::Discord(err) => write!(f, "Discord refused a request: {err}"),
}
}
}
impl core::error::Error for Error {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::Connect(err) => Some(err),
Self::Request(err) => Some(err),
Self::Discord(err) => Some(err),
Self::Unexpected { .. } | Self::Config(_) => None,
}
}
}
impl From<ConnectError> for Error {
fn from(err: ConnectError) -> Self {
Self::Connect(err)
}
}
impl From<RequestError> for Error {
fn from(err: RequestError) -> Self {
Self::Request(err)
}
}
impl From<serenity::Error> for Error {
fn from(err: serenity::Error) -> Self {
Self::Discord(Box::new(err))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::assert_no_dashes;
/// Every arm of this enum reaches a Discord followup, through
/// `bot::interaction::report_failure`, and reaches a terminal through
/// the run loop's own `eprintln!`. That makes all five person-facing
/// strings, and this module had no test at all until now.
///
/// The three wrapping arms are built from a real inner error rather
/// than a stand-in, since what they interpolate is the other crate's
/// wording as much as this one's: a dash arriving from `shep_client`
/// or from serenity prints exactly the same replacement character in
/// exactly the same place.
#[test]
fn no_arm_of_this_enum_carries_a_dash_when_it_is_printed() {
let errors = [
Error::Connect(shep_client::ConnectError::HandshakeClosed),
Error::Request(shep_client::RequestError::Closed),
Error::Unexpected {
asked: "a Flock".to_owned(),
got: "a Pong".to_owned(),
},
Error::Config("token is required".to_owned()),
Error::Discord(Box::new(serenity::Error::Other("refused"))),
];
for err in &errors {
assert_no_dashes(&err.to_string());
}
}
}