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::PanicHookAlreadyInstalled => write!(f, "Panic hook already installed"),
13 Error::InvalidTimestamp(msg) => write!(f, "Invalid Timestamp: {msg}"),
14 Error::InconclusiveMatch(msg) => write!(f, "Inconclusive Match: {msg}"),
15 Error::RateLimit => write!(f, "Rate limited"),
16 Error::BadRequest(msg) => write!(f, "Bad Request: {msg}"),
17 Error::ServerError { status, message } => {
18 write!(f, "Server Error (HTTP {status}): {message}")
19 }
20 Error::Unauthorized => write!(f, "Unauthorized: invalid or missing API token"),
21 Error::BillingLimitExceeded(msg) => {
22 write!(f, "Billing Limit Exceeded: {msg}")
23 }
24 }
25 }
26}
27
28#[derive(Debug)]
30#[non_exhaustive]
31pub enum Error {
32 Connection(String),
34 Serialization(String),
36 AlreadyInitialized,
38 NotInitialized,
40 PanicHookAlreadyInstalled,
42 InvalidTimestamp(String),
44 InconclusiveMatch(String),
46 RateLimit,
48 BadRequest(String),
50 ServerError {
52 status: u16,
54 message: String,
56 },
57 Unauthorized,
59 BillingLimitExceeded(String),
61}
62
63impl Error {
64 pub(crate) fn from_http_response(status: u16, body: String) -> Option<Self> {
67 match status {
68 200..=299 => None,
69 401 => Some(Error::Unauthorized),
70 402 => Some(Error::BillingLimitExceeded(body)),
71 429 => Some(Error::RateLimit),
72 400 | 413 => Some(Error::BadRequest(body)),
73 500..=599 => Some(Error::ServerError {
74 status,
75 message: body,
76 }),
77 _ => Some(Error::Connection(format!(
78 "Unexpected HTTP status {status}: {body}"
79 ))),
80 }
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn success_returns_none() {
90 assert!(Error::from_http_response(200, String::new()).is_none());
91 assert!(Error::from_http_response(201, String::new()).is_none());
92 assert!(Error::from_http_response(299, String::new()).is_none());
93 }
94
95 #[test]
96 fn rate_limit() {
97 let err = Error::from_http_response(429, String::new()).unwrap();
98 assert!(matches!(err, Error::RateLimit));
99 }
100
101 #[test]
102 fn bad_request_preserves_body() {
103 let err = Error::from_http_response(400, "invalid payload".to_string()).unwrap();
104 match err {
105 Error::BadRequest(msg) => assert_eq!(msg, "invalid payload"),
106 _ => panic!("expected BadRequest"),
107 }
108 }
109
110 #[test]
111 fn payload_too_large() {
112 let err = Error::from_http_response(413, "too large".to_string()).unwrap();
113 match err {
114 Error::BadRequest(msg) => assert_eq!(msg, "too large"),
115 _ => panic!("expected BadRequest for 413"),
116 }
117 }
118
119 #[test]
120 fn server_error_preserves_status_and_body() {
121 let err = Error::from_http_response(503, "unavailable".to_string()).unwrap();
122 match err {
123 Error::ServerError { status, message } => {
124 assert_eq!(status, 503);
125 assert_eq!(message, "unavailable");
126 }
127 _ => panic!("expected ServerError"),
128 }
129 }
130
131 #[test]
132 fn unexpected_status_becomes_connection_error() {
133 let err = Error::from_http_response(302, "redirect".to_string()).unwrap();
134 match err {
135 Error::Connection(msg) => {
136 assert!(msg.contains("302"));
137 assert!(msg.contains("redirect"));
138 }
139 _ => panic!("expected Connection"),
140 }
141 }
142}