Skip to main content

edgar_rs/
error.rs

1use reqwest::StatusCode;
2
3/// Errors returned by the SEC EDGAR client.
4#[derive(Debug, thiserror::Error)]
5pub enum Error {
6    /// HTTP request failed (network, DNS, TLS, timeout, etc.).
7    #[error("request to {endpoint} failed: {source}")]
8    Request {
9        endpoint: String,
10        source: reqwest::Error,
11    },
12
13    /// SEC API returned a non-success status code.
14    #[error("edgar: {message} (status {status}, endpoint {endpoint})")]
15    Api {
16        status: StatusCode,
17        endpoint: String,
18        message: String,
19    },
20
21    /// JSON deserialization failed.
22    #[error("failed to parse response from {endpoint}: {source}")]
23    Decode {
24        endpoint: String,
25        source: reqwest::Error,
26    },
27
28    /// Response body could not be interpreted as expected.
29    #[error("failed to decode response from {endpoint}: {message}")]
30    DecodeBody { endpoint: String, message: String },
31
32    /// Client configuration error (e.g., missing user agent).
33    #[error("invalid client configuration: {0}")]
34    Config(String),
35}
36
37impl Error {
38    /// Returns `true` if the SEC API responded with HTTP 429 (Too Many Requests).
39    pub fn is_rate_limited(&self) -> bool {
40        matches!(self, Error::Api { status, .. } if *status == StatusCode::TOO_MANY_REQUESTS)
41    }
42
43    /// Returns `true` if the SEC API responded with HTTP 404 (Not Found).
44    pub fn is_not_found(&self) -> bool {
45        matches!(self, Error::Api { status, .. } if *status == StatusCode::NOT_FOUND)
46    }
47}
48
49/// A specialized `Result` type for SEC EDGAR operations.
50pub 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}