1#[derive(Debug, thiserror::Error)]
2pub enum SdkError {
3 #[error("HTTP error: {0}")]
4 Http(#[from] reqwest::Error),
5
6 #[error("API error (status {status}): {body}")]
7 Api {
8 status: reqwest::StatusCode,
9 body: String,
10 },
11
12 #[error("Failed to decode response: {source}\nBody: {body}")]
13 Decode {
14 #[source]
15 source: serde_json::Error,
16 body: String,
17 },
18
19 #[error("Invalid URL: {0}")]
20 UrlParse(#[from] url::ParseError),
21
22 #[error("Configuration error: {0}")]
23 Config(String),
24
25 #[error("JSON-RPC error (code {code}): {message}")]
26 Rpc { code: i64, message: String },
27
28 #[error("no supported payment option matched the selector; offered: {offered}")]
33 PaymentUnsupported { offered: String },
34
35 #[error("payment rejected by the gateway (status {status}): {body}")]
39 PaymentRejected { status: u16, body: String },
40
41 #[error("payment result indeterminate: request sent but response lost — do not blindly retry (may have been charged)")]
47 PaymentIndeterminate,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum HttpKind {
55 Timeout,
56 Connect,
57 Other,
58}
59
60impl SdkError {
61 pub fn http_kind(&self) -> Option<HttpKind> {
62 match self {
63 SdkError::Http(e) if e.is_timeout() => Some(HttpKind::Timeout),
64 SdkError::Http(e) if e.is_connect() => Some(HttpKind::Connect),
65 SdkError::Http(_) => Some(HttpKind::Other),
66 _ => None,
67 }
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn api_error_display_includes_status_and_body() {
77 let err = SdkError::Api {
78 status: reqwest::StatusCode::NOT_FOUND,
79 body: "not found".to_string(),
80 };
81 let s = err.to_string();
82 assert!(s.contains("404"), "expected 404 in {s}");
83 assert!(s.contains("not found"), "expected body in {s}");
84 }
85
86 #[test]
87 fn config_error_display() {
88 let err = SdkError::Config("missing api key".to_string());
89 assert!(err.to_string().contains("missing api key"));
90 }
91
92 #[test]
93 #[allow(clippy::unwrap_used)]
94 fn http_kind_none_for_non_http_variants() {
95 assert!(SdkError::Config("x".to_string()).http_kind().is_none());
96 let decode_err = SdkError::Decode {
97 source: serde_json::from_str::<i32>("bad").unwrap_err(),
98 body: "bad".to_string(),
99 };
100 assert!(decode_err.http_kind().is_none());
101 }
102}