use std::time::Duration;
use crate::model::ErrorObj;
pub mod exit {
pub const OK: i32 = 0;
pub const UNEXPECTED: i32 = 1;
pub const USAGE: i32 = 2;
pub const PARTIAL: i32 = 3;
pub const ALL_FAILED: i32 = 4;
}
#[derive(Debug, thiserror::Error)]
pub enum RssError {
#[error("usage error: {0}")]
Usage(String),
#[error("invalid URL: {0}")]
InvalidUrl(String),
#[error("network error: {0}")]
Network(String),
#[error("HTTP status {status}: {url}")]
Http {
status: u16,
url: String,
retry_after: Option<String>,
},
#[error("rate limited: {url} (retry after ~{}s)", retry_after.as_secs())]
RateLimited {
url: String,
retry_after: Duration,
},
#[error("feed parse error: {0}")]
Parse(String),
#[error("no feeds discovered at {0}")]
NotFound(String),
#[error("cache error: {0}")]
Cache(String),
#[error(
"response too large: ~{estimated_tokens} tokens exceeds the {budget_tokens}-token \
budget; retry with limit={suggested_limit} or max_content_chars={suggested_max_content_chars}"
)]
ResponseTooLarge {
estimated_tokens: usize,
budget_tokens: usize,
suggested_limit: usize,
suggested_max_content_chars: usize,
},
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("{0}")]
Other(String),
}
impl RssError {
pub fn code(&self) -> &'static str {
match self {
RssError::Usage(_) => "USAGE_ERROR",
RssError::InvalidUrl(_) => "INVALID_URL",
RssError::Network(_) => "NETWORK_ERROR",
RssError::Http { .. } => "FEED_FETCH_FAILED",
RssError::RateLimited { .. } => "RATE_LIMITED",
RssError::Parse(_) => "FEED_PARSE_FAILED",
RssError::NotFound(_) => "NOT_FOUND",
RssError::Cache(_) => "CACHE_ERROR",
RssError::ResponseTooLarge { .. } => "RESPONSE_TOO_LARGE",
RssError::Io(_) => "IO_ERROR",
RssError::Other(_) => "INTERNAL_ERROR",
}
}
pub fn to_error_obj(&self, feed_url: Option<&str>) -> ErrorObj {
let mut obj = ErrorObj::new(self.code(), self.to_string());
if let Some(u) = feed_url {
obj.feed_url = Some(u.to_string());
}
match self {
RssError::Http {
status,
retry_after,
..
} => {
obj.details = serde_json::json!({
"http_status": status,
"retry_after": retry_after,
});
}
RssError::RateLimited { retry_after, .. } => {
obj.details = serde_json::json!({
"retry_after_seconds": retry_after.as_secs(),
"retry_after_ms": u64::try_from(retry_after.as_millis()).unwrap_or(u64::MAX),
});
}
RssError::ResponseTooLarge {
estimated_tokens,
budget_tokens,
suggested_limit,
suggested_max_content_chars,
} => {
obj.details = serde_json::json!({
"estimated_tokens": estimated_tokens,
"budget_tokens": budget_tokens,
"suggested_limit": suggested_limit,
"suggested_max_content_chars": suggested_max_content_chars,
});
}
_ => {}
}
obj
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn http_error_surfaces_retry_after_when_present() {
let e = RssError::Http {
status: 429,
url: "https://x".into(),
retry_after: Some("2".into()),
};
let obj = e.to_error_obj(None);
assert_eq!(obj.code, "FEED_FETCH_FAILED");
assert_eq!(obj.details["http_status"], 429);
assert_eq!(obj.details["retry_after"], "2");
}
#[test]
fn rate_limited_surfaces_code_and_retry_after_details() {
let e = RssError::RateLimited {
url: "https://x/feed".into(),
retry_after: Duration::from_secs(12),
};
let obj = e.to_error_obj(Some("https://x/feed"));
assert_eq!(obj.code, "RATE_LIMITED");
assert_eq!(obj.feed_url.as_deref(), Some("https://x/feed"));
assert_eq!(obj.details["retry_after_seconds"], 12);
assert_eq!(obj.details["retry_after_ms"], 12_000);
}
}