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