use nzb_nntp::error::NntpError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArticleFailureKind {
NotFound,
ServerDown,
AuthFailed,
PermissionDenied,
DecodeError,
Timeout,
ConnectionClosed,
Protocol,
Other,
}
impl ArticleFailureKind {
pub fn is_per_server(self) -> bool {
matches!(
self,
Self::NotFound
| Self::ServerDown
| Self::AuthFailed
| Self::PermissionDenied
| Self::Timeout
| Self::ConnectionClosed
| Self::Protocol
)
}
pub fn counts_toward_hopeless(self) -> bool {
matches!(self, Self::NotFound | Self::DecodeError)
}
pub fn should_break_server(self) -> bool {
matches!(self, Self::AuthFailed | Self::PermissionDenied)
}
pub fn as_str(self) -> &'static str {
match self {
Self::NotFound => "not_found",
Self::ServerDown => "server_down",
Self::AuthFailed => "auth_failed",
Self::PermissionDenied => "permission_denied",
Self::DecodeError => "decode_error",
Self::Timeout => "timeout",
Self::ConnectionClosed => "connection_closed",
Self::Protocol => "protocol",
Self::Other => "other",
}
}
}
#[derive(Debug, Clone)]
pub struct ArticleFailure {
pub kind: ArticleFailureKind,
pub server_id: String,
pub message: String,
}
impl ArticleFailure {
pub fn from_nntp(err: &NntpError, server_id: impl Into<String>) -> Self {
let kind = match err {
NntpError::ArticleNotFound(_) => ArticleFailureKind::NotFound,
NntpError::ServiceUnavailable(_) => ArticleFailureKind::ServerDown,
NntpError::Auth(_) | NntpError::AuthRequired(_) => ArticleFailureKind::AuthFailed,
NntpError::Connection(_) => ArticleFailureKind::ConnectionClosed,
NntpError::Io(_) => ArticleFailureKind::ConnectionClosed,
NntpError::Timeout(_) => ArticleFailureKind::Timeout,
NntpError::Protocol(_) => ArticleFailureKind::Protocol,
NntpError::NoSuchGroup(_) | NntpError::NoArticleSelected(_) => {
ArticleFailureKind::Protocol
}
NntpError::NoConnectionsAvailable(_)
| NntpError::AllServersExhausted(_)
| NntpError::Tls(_)
| NntpError::Shutdown => ArticleFailureKind::Other,
};
Self {
kind,
server_id: server_id.into(),
message: err.to_string(),
}
}
pub fn decode_error(server_id: impl Into<String>, msg: impl Into<String>) -> Self {
Self {
kind: ArticleFailureKind::DecodeError,
server_id: server_id.into(),
message: msg.into(),
}
}
pub fn not_found_anywhere(server_id: impl Into<String>) -> Self {
Self {
kind: ArticleFailureKind::NotFound,
server_id: server_id.into(),
message: "Article not found on any server".to_string(),
}
}
pub fn other(server_id: impl Into<String>, msg: impl Into<String>) -> Self {
Self {
kind: ArticleFailureKind::Other,
server_id: server_id.into(),
message: msg.into(),
}
}
}
impl std::fmt::Display for ArticleFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[{}] {} ({})",
self.kind.as_str(),
self.message,
self.server_id
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_article_not_found() {
let err = NntpError::ArticleNotFound("<msg-1>".into());
let f = ArticleFailure::from_nntp(&err, "srv-a");
assert_eq!(f.kind, ArticleFailureKind::NotFound);
assert_eq!(f.server_id, "srv-a");
}
#[test]
fn classify_service_unavailable_as_server_down() {
let err = NntpError::ServiceUnavailable("502".into());
assert_eq!(
ArticleFailure::from_nntp(&err, "srv-a").kind,
ArticleFailureKind::ServerDown
);
}
#[test]
fn classify_auth() {
let err = NntpError::Auth("482".into());
assert_eq!(
ArticleFailure::from_nntp(&err, "srv-a").kind,
ArticleFailureKind::AuthFailed
);
}
#[test]
fn classify_io_as_connection_closed() {
let err = NntpError::Io(std::io::Error::other("eof"));
assert_eq!(
ArticleFailure::from_nntp(&err, "srv-a").kind,
ArticleFailureKind::ConnectionClosed
);
}
#[test]
fn classify_timeout() {
let err = NntpError::Timeout("read timeout".into());
assert_eq!(
ArticleFailure::from_nntp(&err, "srv-a").kind,
ArticleFailureKind::Timeout
);
}
#[test]
fn per_server_classification_is_correct() {
assert!(ArticleFailureKind::NotFound.is_per_server());
assert!(ArticleFailureKind::ServerDown.is_per_server());
assert!(!ArticleFailureKind::DecodeError.is_per_server());
assert!(!ArticleFailureKind::Other.is_per_server());
}
#[test]
fn counts_toward_hopeless() {
assert!(ArticleFailureKind::NotFound.counts_toward_hopeless());
assert!(ArticleFailureKind::DecodeError.counts_toward_hopeless());
assert!(!ArticleFailureKind::ServerDown.counts_toward_hopeless());
assert!(!ArticleFailureKind::AuthFailed.counts_toward_hopeless());
}
}