1use std::fmt;
8
9use serde_json::Value;
10
11#[derive(Debug)]
13pub enum PulseError {
14 Auth { path: String, body: Option<Value> },
16 NotFound { path: String, body: Option<Value> },
18 Validation { path: String, body: Option<Value> },
20 RateLimit {
24 path: String,
25 body: Option<Value>,
26 retry_after_seconds: Option<u32>,
27 },
28 Api {
30 status: u16,
31 path: String,
32 body: Option<Value>,
33 },
34 Transport(reqwest::Error),
37 Json(serde_json::Error),
40 NoToken { path: String },
43 InvalidConfig(String),
45}
46
47impl PulseError {
48 pub fn is_auth_error(&self) -> bool {
49 matches!(self, PulseError::Auth { .. } | PulseError::NoToken { .. })
50 }
51
52 pub fn is_not_found(&self) -> bool {
53 matches!(self, PulseError::NotFound { .. })
54 }
55
56 pub fn is_validation_error(&self) -> bool {
57 matches!(self, PulseError::Validation { .. })
58 }
59
60 pub fn is_rate_limited(&self) -> bool {
61 matches!(self, PulseError::RateLimit { .. })
62 }
63
64 pub fn status_code(&self) -> Option<u16> {
67 match self {
68 PulseError::Auth { .. } | PulseError::NoToken { .. } => Some(401),
69 PulseError::NotFound { .. } => Some(404),
70 PulseError::Validation { .. } => Some(400),
71 PulseError::RateLimit { .. } => Some(429),
72 PulseError::Api { status, .. } => Some(*status),
73 PulseError::Transport(_) | PulseError::Json(_) | PulseError::InvalidConfig(_) => None,
74 }
75 }
76
77 pub fn body(&self) -> Option<&Value> {
79 match self {
80 PulseError::Auth { body, .. }
81 | PulseError::NotFound { body, .. }
82 | PulseError::Validation { body, .. }
83 | PulseError::RateLimit { body, .. }
84 | PulseError::Api { body, .. } => body.as_ref(),
85 _ => None,
86 }
87 }
88
89 pub fn path(&self) -> Option<&str> {
90 match self {
91 PulseError::Auth { path, .. }
92 | PulseError::NotFound { path, .. }
93 | PulseError::Validation { path, .. }
94 | PulseError::RateLimit { path, .. }
95 | PulseError::Api { path, .. }
96 | PulseError::NoToken { path } => Some(path),
97 _ => None,
98 }
99 }
100}
101
102impl fmt::Display for PulseError {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 let summary = match self {
105 PulseError::Auth { path, body } => format_http(401, path, body.as_ref()),
106 PulseError::NotFound { path, body } => format_http(404, path, body.as_ref()),
107 PulseError::Validation { path, body } => format_http(400, path, body.as_ref()),
108 PulseError::RateLimit { path, body, .. } => format_http(429, path, body.as_ref()),
109 PulseError::Api { status, path, body } => format_http(*status, path, body.as_ref()),
110 PulseError::Transport(e) => return write!(f, "pulse: HTTP transport failure — {e}"),
111 PulseError::Json(e) => return write!(f, "pulse: JSON encode/decode failure — {e}"),
112 PulseError::NoToken { path } => {
113 return write!(
114 f,
115 "pulse: no token set for {path} — call client.auth().login(...).await first \
116 or pass .token(...) to the builder"
117 );
118 }
119 PulseError::InvalidConfig(msg) => return write!(f, "pulse: invalid config — {msg}"),
120 };
121 write!(f, "{summary}")
122 }
123}
124
125fn format_http(status: u16, path: &str, body: Option<&Value>) -> String {
126 let mut msg = format!("pulse: HTTP {status} from {path}");
127 if let Some(v) = body {
128 if let Some(err) = v
129 .get("error")
130 .and_then(Value::as_str)
131 .or_else(|| v.get("errorMessage").and_then(Value::as_str))
132 .or_else(|| v.get("message").and_then(Value::as_str))
133 {
134 msg.push_str(" — ");
135 msg.push_str(err);
136 }
137 }
138 msg
139}
140
141impl std::error::Error for PulseError {
142 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
143 match self {
144 PulseError::Transport(e) => Some(e),
145 PulseError::Json(e) => Some(e),
146 _ => None,
147 }
148 }
149}
150
151impl From<reqwest::Error> for PulseError {
152 fn from(e: reqwest::Error) -> Self {
153 PulseError::Transport(e)
154 }
155}
156
157impl From<serde_json::Error> for PulseError {
158 fn from(e: serde_json::Error) -> Self {
159 PulseError::Json(e)
160 }
161}