1use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10pub const CODE_NOT_IN_ROOM: i32 = 2;
12pub const CODE_REACHED_NOBODY: i32 = 3;
14pub const CODE_TIMED_OUT: i32 = 4;
16pub const CODE_NAME_TAKEN: i32 = 5;
18pub const CODE_DENIED: i32 = 6;
20pub const CODE_ROOM_PAUSED: i32 = 7;
22
23#[derive(Debug, Error)]
24pub enum Error {
25 #[error("not in room: {0}")]
26 NotInRoom(String),
27 #[error("reached nobody: {0}")]
28 ReachedNobody(String),
29 #[error("timed out")]
30 TimedOut,
31 #[error("name already taken: {0}")]
32 NameTaken(String),
33 #[error("denied: {0}")]
34 Denied(String),
35 #[error("room paused: {0}")]
36 RoomPaused(String),
37 #[error("invalid: {0}")]
38 Invalid(String),
39 #[error("bad signature: {0}")]
40 BadSignature(String),
41 #[error("over budget: {0}")]
44 OverBudget(String, Option<u64>),
45 #[error("storage: {0}")]
46 Db(#[from] rusqlite::Error),
47 #[error("migration: {0}")]
48 Migration(#[from] rusqlite_migration::Error),
49 #[error("io: {0}")]
50 Io(#[from] std::io::Error),
51 #[error("json: {0}")]
52 Json(#[from] serde_json::Error),
53 #[error("{0}")]
54 Other(String),
55}
56
57impl Error {
58 pub fn code(&self) -> i32 {
60 match self {
61 Error::NotInRoom(_) => CODE_NOT_IN_ROOM,
62 Error::ReachedNobody(_) => CODE_REACHED_NOBODY,
63 Error::TimedOut => CODE_TIMED_OUT,
64 Error::NameTaken(_) => CODE_NAME_TAKEN,
65 Error::Denied(_) | Error::BadSignature(_) => CODE_DENIED,
66 Error::RoomPaused(_) => CODE_ROOM_PAUSED,
67 _ => 1,
68 }
69 }
70
71 pub fn fate(&self) -> Option<Fate> {
79 match self {
80 Error::ReachedNobody(_)
81 | Error::TimedOut
82 | Error::RoomPaused(_)
83 | Error::OverBudget(..)
84 | Error::Io(_)
85 | Error::Db(_)
86 | Error::Migration(_) => Some(Fate::Temporary),
87 Error::NotInRoom(_)
88 | Error::NameTaken(_)
89 | Error::Denied(_)
90 | Error::BadSignature(_)
91 | Error::Invalid(_) => Some(Fate::Definitive),
92 Error::Json(_) | Error::Other(_) => None,
93 }
94 }
95
96 pub fn retry_after(&self) -> Option<u64> {
98 match self {
99 Error::OverBudget(_, secs) => *secs,
100 _ => None,
101 }
102 }
103
104 pub fn from_code(code: i32, msg: &str) -> Self {
108 let msg = strip_labels(msg);
109 let msg = msg.as_str();
110 match code {
111 CODE_NOT_IN_ROOM => Error::NotInRoom(msg.to_string()),
112 CODE_REACHED_NOBODY => Error::ReachedNobody(msg.to_string()),
113 CODE_TIMED_OUT => Error::TimedOut,
114 CODE_NAME_TAKEN => Error::NameTaken(msg.to_string()),
115 CODE_DENIED => Error::Denied(msg.to_string()),
116 CODE_ROOM_PAUSED => Error::RoomPaused(msg.to_string()),
117 _ => Error::Other(msg.to_string()),
118 }
119 }
120}
121
122const LABELS: &[&str] = &[
123 "not in room: ",
124 "reached nobody: ",
125 "name already taken: ",
126 "denied: ",
127 "room paused: ",
128 "invalid: ",
129 "bad signature: ",
130 "over budget: ",
131 "storage: ",
132 "io: ",
133 "json: ",
134];
135
136fn strip_labels(msg: &str) -> String {
137 let mut m = msg;
138 loop {
139 let mut stripped = false;
140 for label in LABELS {
141 if let Some(rest) = m.strip_prefix(label) {
142 m = rest;
143 stripped = true;
144 }
145 }
146 if !stripped {
147 return m.to_string();
148 }
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "lowercase")]
155pub enum Fate {
156 Temporary,
158 Definitive,
160}
161
162pub type Result<T> = std::result::Result<T, Error>;
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn codes_roundtrip_without_stacking() {
170 let e = Error::NameTaken("bob".into());
171 let hop1 = Error::from_code(e.code(), &e.to_string());
172 let hop2 = Error::from_code(hop1.code(), &hop1.to_string());
173 assert_eq!(hop2.to_string(), "name already taken: bob");
174 assert_eq!(hop2.code(), CODE_NAME_TAKEN);
175 assert_eq!(Error::from_code(4, "timed out").code(), CODE_TIMED_OUT);
176 }
177}