nzb_dispatch/
article_failure.rs1use nzb_nntp::error::NntpError;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum ArticleFailureKind {
20 NotFound,
23 ServerDown,
26 AuthFailed,
29 PermissionDenied,
33 DecodeError,
36 Timeout,
38 ConnectionClosed,
40 Protocol,
42 Other,
44}
45
46impl ArticleFailureKind {
47 pub fn is_per_server(self) -> bool {
50 matches!(
51 self,
52 Self::NotFound
53 | Self::ServerDown
54 | Self::AuthFailed
55 | Self::PermissionDenied
56 | Self::Timeout
57 | Self::ConnectionClosed
58 | Self::Protocol
59 )
60 }
61
62 pub fn counts_toward_hopeless(self) -> bool {
65 matches!(self, Self::NotFound | Self::DecodeError)
66 }
67
68 pub fn should_break_server(self) -> bool {
71 matches!(self, Self::AuthFailed | Self::PermissionDenied)
72 }
73
74 pub fn as_str(self) -> &'static str {
76 match self {
77 Self::NotFound => "not_found",
78 Self::ServerDown => "server_down",
79 Self::AuthFailed => "auth_failed",
80 Self::PermissionDenied => "permission_denied",
81 Self::DecodeError => "decode_error",
82 Self::Timeout => "timeout",
83 Self::ConnectionClosed => "connection_closed",
84 Self::Protocol => "protocol",
85 Self::Other => "other",
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct ArticleFailure {
93 pub kind: ArticleFailureKind,
94 pub server_id: String,
95 pub message: String,
96}
97
98impl ArticleFailure {
99 pub fn from_nntp(err: &NntpError, server_id: impl Into<String>) -> Self {
101 let kind = match err {
102 NntpError::ArticleNotFound(_) => ArticleFailureKind::NotFound,
103 NntpError::ServiceUnavailable(_) => ArticleFailureKind::ServerDown,
104 NntpError::Auth(_) | NntpError::AuthRequired(_) => ArticleFailureKind::AuthFailed,
105 NntpError::Connection(_) => ArticleFailureKind::ConnectionClosed,
106 NntpError::Io(_) => ArticleFailureKind::ConnectionClosed,
107 NntpError::Timeout(_) => ArticleFailureKind::Timeout,
108 NntpError::Protocol(_) => ArticleFailureKind::Protocol,
109 NntpError::NoSuchGroup(_) | NntpError::NoArticleSelected(_) => {
110 ArticleFailureKind::Protocol
111 }
112 NntpError::NoConnectionsAvailable(_)
113 | NntpError::AllServersExhausted(_)
114 | NntpError::Tls(_)
115 | NntpError::Shutdown => ArticleFailureKind::Other,
116 };
117 Self {
118 kind,
119 server_id: server_id.into(),
120 message: err.to_string(),
121 }
122 }
123
124 pub fn decode_error(server_id: impl Into<String>, msg: impl Into<String>) -> Self {
126 Self {
127 kind: ArticleFailureKind::DecodeError,
128 server_id: server_id.into(),
129 message: msg.into(),
130 }
131 }
132
133 pub fn not_found_anywhere(server_id: impl Into<String>) -> Self {
136 Self {
137 kind: ArticleFailureKind::NotFound,
138 server_id: server_id.into(),
139 message: "Article not found on any server".to_string(),
140 }
141 }
142
143 pub fn other(server_id: impl Into<String>, msg: impl Into<String>) -> Self {
145 Self {
146 kind: ArticleFailureKind::Other,
147 server_id: server_id.into(),
148 message: msg.into(),
149 }
150 }
151}
152
153impl std::fmt::Display for ArticleFailure {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 write!(
156 f,
157 "[{}] {} ({})",
158 self.kind.as_str(),
159 self.message,
160 self.server_id
161 )
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn classify_article_not_found() {
171 let err = NntpError::ArticleNotFound("<msg-1>".into());
172 let f = ArticleFailure::from_nntp(&err, "srv-a");
173 assert_eq!(f.kind, ArticleFailureKind::NotFound);
174 assert_eq!(f.server_id, "srv-a");
175 }
176
177 #[test]
178 fn classify_service_unavailable_as_server_down() {
179 let err = NntpError::ServiceUnavailable("502".into());
180 assert_eq!(
181 ArticleFailure::from_nntp(&err, "srv-a").kind,
182 ArticleFailureKind::ServerDown
183 );
184 }
185
186 #[test]
187 fn classify_auth() {
188 let err = NntpError::Auth("482".into());
189 assert_eq!(
190 ArticleFailure::from_nntp(&err, "srv-a").kind,
191 ArticleFailureKind::AuthFailed
192 );
193 }
194
195 #[test]
196 fn classify_io_as_connection_closed() {
197 let err = NntpError::Io(std::io::Error::other("eof"));
198 assert_eq!(
199 ArticleFailure::from_nntp(&err, "srv-a").kind,
200 ArticleFailureKind::ConnectionClosed
201 );
202 }
203
204 #[test]
205 fn classify_timeout() {
206 let err = NntpError::Timeout("read timeout".into());
207 assert_eq!(
208 ArticleFailure::from_nntp(&err, "srv-a").kind,
209 ArticleFailureKind::Timeout
210 );
211 }
212
213 #[test]
214 fn per_server_classification_is_correct() {
215 assert!(ArticleFailureKind::NotFound.is_per_server());
216 assert!(ArticleFailureKind::ServerDown.is_per_server());
217 assert!(!ArticleFailureKind::DecodeError.is_per_server());
218 assert!(!ArticleFailureKind::Other.is_per_server());
219 }
220
221 #[test]
222 fn counts_toward_hopeless() {
223 assert!(ArticleFailureKind::NotFound.counts_toward_hopeless());
224 assert!(ArticleFailureKind::DecodeError.counts_toward_hopeless());
225 assert!(!ArticleFailureKind::ServerDown.counts_toward_hopeless());
226 assert!(!ArticleFailureKind::AuthFailed.counts_toward_hopeless());
227 }
228}