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::PermissionDenied(_) => ArticleFailureKind::PermissionDenied,
106 NntpError::Connection(_) => ArticleFailureKind::ConnectionClosed,
107 NntpError::Io(_) => ArticleFailureKind::ConnectionClosed,
108 NntpError::Timeout(_) => ArticleFailureKind::Timeout,
109 NntpError::Protocol(_)
110 | NntpError::UnsupportedCommand(_)
111 | NntpError::ResponseTooLarge(_) => ArticleFailureKind::Protocol,
112 NntpError::NoSuchGroup(_) | NntpError::NoArticleSelected(_) => {
113 ArticleFailureKind::Protocol
114 }
115 NntpError::NoConnectionsAvailable(_)
116 | NntpError::AllServersExhausted(_)
117 | NntpError::Tls(_)
118 | NntpError::Shutdown => ArticleFailureKind::Other,
119 };
120 Self {
121 kind,
122 server_id: server_id.into(),
123 message: err.to_string(),
124 }
125 }
126
127 pub fn decode_error(server_id: impl Into<String>, msg: impl Into<String>) -> Self {
129 Self {
130 kind: ArticleFailureKind::DecodeError,
131 server_id: server_id.into(),
132 message: msg.into(),
133 }
134 }
135
136 pub fn not_found_anywhere(
139 server_id: impl Into<String>,
140 provider_outcomes: impl Into<String>,
141 ) -> Self {
142 Self {
143 kind: ArticleFailureKind::NotFound,
144 server_id: server_id.into(),
145 message: format!(
146 "Article explicitly not found on every eligible provider; outcomes: {}",
147 provider_outcomes.into()
148 ),
149 }
150 }
151
152 pub fn other(server_id: impl Into<String>, msg: impl Into<String>) -> Self {
154 Self {
155 kind: ArticleFailureKind::Other,
156 server_id: server_id.into(),
157 message: msg.into(),
158 }
159 }
160}
161
162impl std::fmt::Display for ArticleFailure {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 write!(
165 f,
166 "[{}] {} ({})",
167 self.kind.as_str(),
168 self.message,
169 self.server_id
170 )
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn classify_article_not_found() {
180 let err = NntpError::ArticleNotFound("<msg-1>".into());
181 let f = ArticleFailure::from_nntp(&err, "srv-a");
182 assert_eq!(f.kind, ArticleFailureKind::NotFound);
183 assert_eq!(f.server_id, "srv-a");
184 }
185
186 #[test]
187 fn classify_service_unavailable_as_server_down() {
188 let err = NntpError::ServiceUnavailable("502".into());
189 assert_eq!(
190 ArticleFailure::from_nntp(&err, "srv-a").kind,
191 ArticleFailureKind::ServerDown
192 );
193 }
194
195 #[test]
196 fn classify_auth() {
197 let err = NntpError::Auth("482".into());
198 assert_eq!(
199 ArticleFailure::from_nntp(&err, "srv-a").kind,
200 ArticleFailureKind::AuthFailed
201 );
202 }
203
204 #[test]
205 fn classify_io_as_connection_closed() {
206 let err = NntpError::Io(std::io::Error::other("eof"));
207 assert_eq!(
208 ArticleFailure::from_nntp(&err, "srv-a").kind,
209 ArticleFailureKind::ConnectionClosed
210 );
211 }
212
213 #[test]
214 fn classify_timeout() {
215 let err = NntpError::Timeout("read timeout".into());
216 assert_eq!(
217 ArticleFailure::from_nntp(&err, "srv-a").kind,
218 ArticleFailureKind::Timeout
219 );
220 }
221
222 #[test]
223 fn per_server_classification_is_correct() {
224 assert!(ArticleFailureKind::NotFound.is_per_server());
225 assert!(ArticleFailureKind::ServerDown.is_per_server());
226 assert!(!ArticleFailureKind::DecodeError.is_per_server());
227 assert!(!ArticleFailureKind::Other.is_per_server());
228 }
229
230 #[test]
231 fn counts_toward_hopeless() {
232 assert!(ArticleFailureKind::NotFound.counts_toward_hopeless());
233 assert!(ArticleFailureKind::DecodeError.counts_toward_hopeless());
234 assert!(!ArticleFailureKind::ServerDown.counts_toward_hopeless());
235 assert!(!ArticleFailureKind::AuthFailed.counts_toward_hopeless());
236 }
237}