use reqwest::StatusCode;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("request to {endpoint} failed: {source}")]
Request {
endpoint: String,
source: reqwest::Error,
},
#[error("edgar: {message} (status {status}, endpoint {endpoint})")]
Api {
status: StatusCode,
endpoint: String,
message: String,
},
#[error("failed to parse response from {endpoint}: {source}")]
Decode {
endpoint: String,
source: reqwest::Error,
},
#[error("failed to decode response from {endpoint}: {message}")]
DecodeBody { endpoint: String, message: String },
#[error("invalid client configuration: {0}")]
Config(String),
}
impl Error {
pub fn is_rate_limited(&self) -> bool {
matches!(self, Error::Api { status, .. } if *status == StatusCode::TOO_MANY_REQUESTS)
}
pub fn is_not_found(&self) -> bool {
matches!(self, Error::Api { status, .. } if *status == StatusCode::NOT_FOUND)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_rate_limited_returns_true_for_429() {
let err = Error::Api {
status: StatusCode::TOO_MANY_REQUESTS,
endpoint: "https://example.com".into(),
message: "rate limited".into(),
};
assert!(err.is_rate_limited());
assert!(!err.is_not_found());
}
#[test]
fn is_not_found_returns_true_for_404() {
let err = Error::Api {
status: StatusCode::NOT_FOUND,
endpoint: "https://example.com".into(),
message: "not found".into(),
};
assert!(err.is_not_found());
assert!(!err.is_rate_limited());
}
#[test]
fn is_rate_limited_returns_false_for_other_status() {
let err = Error::Api {
status: StatusCode::INTERNAL_SERVER_ERROR,
endpoint: "https://example.com".into(),
message: "server error".into(),
};
assert!(!err.is_rate_limited());
assert!(!err.is_not_found());
}
#[test]
fn is_rate_limited_returns_false_for_non_api_errors() {
let err = Error::Config("test".into());
assert!(!err.is_rate_limited());
assert!(!err.is_not_found());
}
#[test]
fn config_error_display() {
let err = Error::Config("missing user agent".into());
assert_eq!(
err.to_string(),
"invalid client configuration: missing user agent"
);
}
#[test]
fn api_error_display() {
let err = Error::Api {
status: StatusCode::NOT_FOUND,
endpoint: "https://data.sec.gov/test".into(),
message: "unexpected status 404 Not Found".into(),
};
let msg = err.to_string();
assert!(msg.contains("404 Not Found"));
assert!(msg.contains("https://data.sec.gov/test"));
}
#[test]
fn decode_body_error_display() {
let err = Error::DecodeBody {
endpoint: "https://example.com".into(),
message: "not valid UTF-8".into(),
};
let msg = err.to_string();
assert!(msg.contains("not valid UTF-8"));
assert!(msg.contains("https://example.com"));
}
#[test]
fn error_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Error>();
}
}