1use reqwest::StatusCode;
2
3#[derive(Debug, thiserror::Error)]
5pub enum Error {
6 #[error("request to {endpoint} failed: {source}")]
8 Request {
9 endpoint: String,
10 source: reqwest::Error,
11 },
12
13 #[error("edgar: {message} (status {status}, endpoint {endpoint})")]
15 Api {
16 status: StatusCode,
17 endpoint: String,
18 message: String,
19 },
20
21 #[error("failed to parse response from {endpoint}: {source}")]
23 Decode {
24 endpoint: String,
25 source: reqwest::Error,
26 },
27
28 #[error("failed to decode response from {endpoint}: {message}")]
30 DecodeBody { endpoint: String, message: String },
31
32 #[error("invalid client configuration: {0}")]
34 Config(String),
35}
36
37impl Error {
38 pub fn is_rate_limited(&self) -> bool {
40 matches!(self, Error::Api { status, .. } if *status == StatusCode::TOO_MANY_REQUESTS)
41 }
42
43 pub fn is_not_found(&self) -> bool {
45 matches!(self, Error::Api { status, .. } if *status == StatusCode::NOT_FOUND)
46 }
47}
48
49pub type Result<T> = std::result::Result<T, Error>;
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn is_rate_limited_returns_true_for_429() {
58 let err = Error::Api {
59 status: StatusCode::TOO_MANY_REQUESTS,
60 endpoint: "https://example.com".into(),
61 message: "rate limited".into(),
62 };
63 assert!(err.is_rate_limited());
64 assert!(!err.is_not_found());
65 }
66
67 #[test]
68 fn is_not_found_returns_true_for_404() {
69 let err = Error::Api {
70 status: StatusCode::NOT_FOUND,
71 endpoint: "https://example.com".into(),
72 message: "not found".into(),
73 };
74 assert!(err.is_not_found());
75 assert!(!err.is_rate_limited());
76 }
77
78 #[test]
79 fn is_rate_limited_returns_false_for_other_status() {
80 let err = Error::Api {
81 status: StatusCode::INTERNAL_SERVER_ERROR,
82 endpoint: "https://example.com".into(),
83 message: "server error".into(),
84 };
85 assert!(!err.is_rate_limited());
86 assert!(!err.is_not_found());
87 }
88
89 #[test]
90 fn is_rate_limited_returns_false_for_non_api_errors() {
91 let err = Error::Config("test".into());
92 assert!(!err.is_rate_limited());
93 assert!(!err.is_not_found());
94 }
95
96 #[test]
97 fn config_error_display() {
98 let err = Error::Config("missing user agent".into());
99 assert_eq!(
100 err.to_string(),
101 "invalid client configuration: missing user agent"
102 );
103 }
104
105 #[test]
106 fn api_error_display() {
107 let err = Error::Api {
108 status: StatusCode::NOT_FOUND,
109 endpoint: "https://data.sec.gov/test".into(),
110 message: "unexpected status 404 Not Found".into(),
111 };
112 let msg = err.to_string();
113 assert!(msg.contains("404 Not Found"));
114 assert!(msg.contains("https://data.sec.gov/test"));
115 }
116
117 #[test]
118 fn decode_body_error_display() {
119 let err = Error::DecodeBody {
120 endpoint: "https://example.com".into(),
121 message: "not valid UTF-8".into(),
122 };
123 let msg = err.to_string();
124 assert!(msg.contains("not valid UTF-8"));
125 assert!(msg.contains("https://example.com"));
126 }
127
128 #[test]
129 fn error_is_send_and_sync() {
130 fn assert_send_sync<T: Send + Sync>() {}
131 assert_send_sync::<Error>();
132 }
133}