1use std::fmt::{Display, Formatter};
2
3impl std::error::Error for Error {}
4
5impl Display for Error {
6 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
7 match self {
8 Error::Connection(msg) => write!(f, "Connection Error: {msg}"),
9 Error::Serialization(msg) => write!(f, "Serialization Error: {msg}"),
10 Error::AlreadyInitialized => write!(f, "Client already initialized"),
11 Error::NotInitialized => write!(f, "Client not initialized"),
12 Error::InvalidTimestamp(msg) => write!(f, "Invalid Timestamp: {msg}"),
13 Error::InconclusiveMatch(msg) => write!(f, "Inconclusive Match: {msg}"),
14 Error::RateLimit => write!(f, "Rate limited"),
15 Error::BadRequest(msg) => write!(f, "Bad Request: {msg}"),
16 Error::ServerError { status, message } => {
17 write!(f, "Server Error (HTTP {status}): {message}")
18 }
19 Error::Unauthorized => write!(f, "Unauthorized: invalid or missing API token"),
20 Error::BillingLimitExceeded(msg) => {
21 write!(f, "Billing Limit Exceeded: {msg}")
22 }
23 }
24 }
25}
26
27#[derive(Debug)]
29#[non_exhaustive]
30pub enum Error {
31 Connection(String),
33 Serialization(String),
35 AlreadyInitialized,
37 NotInitialized,
39 InvalidTimestamp(String),
41 InconclusiveMatch(String),
43 RateLimit,
45 BadRequest(String),
47 ServerError {
49 status: u16,
51 message: String,
53 },
54 Unauthorized,
56 BillingLimitExceeded(String),
58}
59
60impl Error {
61 pub(crate) fn from_http_response(status: u16, body: String) -> Option<Self> {
64 match status {
65 200..=299 => None,
66 401 => Some(Error::Unauthorized),
67 402 => Some(Error::BillingLimitExceeded(body)),
68 429 => Some(Error::RateLimit),
69 400 | 413 => Some(Error::BadRequest(body)),
70 500..=599 => Some(Error::ServerError {
71 status,
72 message: body,
73 }),
74 _ => Some(Error::Connection(format!(
75 "Unexpected HTTP status {status}: {body}"
76 ))),
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn success_returns_none() {
87 assert!(Error::from_http_response(200, String::new()).is_none());
88 assert!(Error::from_http_response(201, String::new()).is_none());
89 assert!(Error::from_http_response(299, String::new()).is_none());
90 }
91
92 #[test]
93 fn rate_limit() {
94 let err = Error::from_http_response(429, String::new()).unwrap();
95 assert!(matches!(err, Error::RateLimit));
96 }
97
98 #[test]
99 fn bad_request_preserves_body() {
100 let err = Error::from_http_response(400, "invalid payload".to_string()).unwrap();
101 match err {
102 Error::BadRequest(msg) => assert_eq!(msg, "invalid payload"),
103 _ => panic!("expected BadRequest"),
104 }
105 }
106
107 #[test]
108 fn payload_too_large() {
109 let err = Error::from_http_response(413, "too large".to_string()).unwrap();
110 match err {
111 Error::BadRequest(msg) => assert_eq!(msg, "too large"),
112 _ => panic!("expected BadRequest for 413"),
113 }
114 }
115
116 #[test]
117 fn server_error_preserves_status_and_body() {
118 let err = Error::from_http_response(503, "unavailable".to_string()).unwrap();
119 match err {
120 Error::ServerError { status, message } => {
121 assert_eq!(status, 503);
122 assert_eq!(message, "unavailable");
123 }
124 _ => panic!("expected ServerError"),
125 }
126 }
127
128 #[test]
129 fn unexpected_status_becomes_connection_error() {
130 let err = Error::from_http_response(302, "redirect".to_string()).unwrap();
131 match err {
132 Error::Connection(msg) => {
133 assert!(msg.contains("302"));
134 assert!(msg.contains("redirect"));
135 }
136 _ => panic!("expected Connection"),
137 }
138 }
139}