1use std::ffi::{CString, c_char};
17use std::ptr;
18
19use aviso::ClientError;
20use aviso::watch::{TriggerError, TriggerKindLabel};
21
22use crate::outcome::AvisoOutcome;
23
24#[repr(C)]
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum AvisoErrorKind {
33 Transport = 0,
35 Http = 1,
37 Auth = 2,
39 Decode = 3,
41 MalformedEvent = 4,
43 HistoryGap = 5,
45 StreamProtocol = 6,
47 Config = 7,
49 StateStore = 8,
51 Trigger = 9,
53 InvalidInput = 10,
55 InvalidUsage = 11,
58 Internal = 12,
60 Panic = 13,
62 Unknown = 14,
64}
65
66#[repr(C)]
71pub struct AvisoError {
72 pub kind: AvisoErrorKind,
74 pub http_status: u16,
76 pub message: *const c_char,
79 pub request_id: *const c_char,
81 pub trigger_kind: *const c_char,
83 pub error_kind: *const c_char,
85}
86
87pub(crate) struct OutcomeError {
90 message: CString,
91 request_id: Option<CString>,
92 trigger_kind: Option<CString>,
93 error_kind: Option<CString>,
94 view: AvisoError,
95}
96
97impl OutcomeError {
98 pub(crate) fn build(
99 kind: AvisoErrorKind,
100 http_status: u16,
101 message: String,
102 request_id: Option<String>,
103 trigger_kind: Option<&str>,
104 error_kind: Option<String>,
105 ) -> Self {
106 let mut error = OutcomeError {
107 message: cstring_lossy(message),
108 request_id: request_id.map(cstring_lossy),
109 trigger_kind: trigger_kind.map(|s| cstring_lossy(s.to_string())),
110 error_kind: error_kind.map(cstring_lossy),
111 view: AvisoError {
112 kind,
113 http_status,
114 message: ptr::null(),
115 request_id: ptr::null(),
116 trigger_kind: ptr::null(),
117 error_kind: ptr::null(),
118 },
119 };
120 error.view.message = error.message.as_ptr();
125 error.view.request_id = opt_ptr(error.request_id.as_ref());
126 error.view.trigger_kind = opt_ptr(error.trigger_kind.as_ref());
127 error.view.error_kind = opt_ptr(error.error_kind.as_ref());
128 error
129 }
130
131 pub(crate) fn view(&self) -> *const AvisoError {
133 &raw const self.view
134 }
135
136 pub(crate) fn kind(&self) -> AvisoErrorKind {
137 self.view.kind
138 }
139
140 pub(crate) fn http_status(&self) -> u16 {
141 self.view.http_status
142 }
143
144 pub(crate) fn message_string(&self) -> String {
145 self.message.to_string_lossy().into_owned()
146 }
147
148 pub(crate) fn request_id_string(&self) -> Option<String> {
149 self.request_id
150 .as_ref()
151 .map(|value| value.to_string_lossy().into_owned())
152 }
153
154 pub(crate) fn into_outcome(self) -> *mut AvisoOutcome {
156 AvisoOutcome::error(self).into_raw()
157 }
158}
159
160fn opt_ptr(s: Option<&CString>) -> *const c_char {
161 s.map_or(ptr::null(), |c| c.as_ptr())
162}
163
164pub(crate) fn cstring_lossy(s: String) -> CString {
167 let bytes: Vec<u8> = s.into_bytes().into_iter().filter(|b| *b != 0).collect();
168 CString::new(bytes).unwrap_or_default()
169}
170
171pub(crate) fn map_error(err: &ClientError) -> OutcomeError {
174 use AvisoErrorKind as K;
175 match err {
176 ClientError::Transport(inner) => {
177 OutcomeError::build(K::Transport, 0, inner.to_string(), None, None, None)
178 }
179 ClientError::Http {
180 status,
181 body,
182 request_id,
183 } => OutcomeError::build(
184 K::Http,
185 *status,
186 format!("http {status}: {body}"),
187 request_id.clone(),
188 None,
189 None,
190 ),
191 ClientError::Auth(message) => {
192 OutcomeError::build(K::Auth, 0, message.clone(), None, None, None)
193 }
194 ClientError::Decode(inner) => {
195 OutcomeError::build(K::Decode, 0, inner.to_string(), None, None, None)
196 }
197 ClientError::MalformedEvent(detail) => {
198 OutcomeError::build(K::MalformedEvent, 0, detail.clone(), None, None, None)
199 }
200 ClientError::HistoryGap { reason } => OutcomeError::build(
201 K::HistoryGap,
202 0,
203 format!("history gap: {reason:?}"),
204 None,
205 None,
206 None,
207 ),
208 ClientError::StreamProtocol {
209 message,
210 request_id,
211 } => OutcomeError::build(
212 K::StreamProtocol,
213 0,
214 message.clone(),
215 request_id.clone(),
216 None,
217 None,
218 ),
219 ClientError::Config(message) => {
220 OutcomeError::build(K::Config, 0, message.clone(), None, None, None)
221 }
222 ClientError::StateStore(inner) => {
223 OutcomeError::build(K::StateStore, 0, inner.to_string(), None, None, None)
224 }
225 ClientError::TriggerFailed { kind, source } => OutcomeError::build(
226 K::Trigger,
227 0,
228 err.to_string(),
229 None,
230 Some(trigger_kind_label(kind)),
231 Some(trigger_error_label(source).to_string()),
232 ),
233 _ => OutcomeError::build(K::Unknown, 0, err.to_string(), None, None, None),
234 }
235}
236
237fn trigger_error_label(source: &TriggerError) -> &'static str {
240 match source {
241 TriggerError::Io(_) => "io",
242 TriggerError::Encode(_) => "encode",
243 #[cfg(unix)]
244 TriggerError::Command { .. } => "command",
245 TriggerError::Timeout(_) => "timeout",
246 TriggerError::Webhook { .. } => "webhook",
247 TriggerError::WebhookBuild { .. } => "webhook_build",
248 TriggerError::Template { .. } => "template",
249 _ => "unknown",
250 }
251}
252
253fn trigger_kind_label(label: &TriggerKindLabel) -> &'static str {
256 match label {
257 TriggerKindLabel::Echo => "echo",
258 TriggerKindLabel::Log { .. } => "log",
259 #[cfg(unix)]
260 TriggerKindLabel::Command => "command",
261 TriggerKindLabel::Webhook => "webhook",
262 TriggerKindLabel::Teams => "teams",
263 TriggerKindLabel::Post => "post",
264 _ => "unknown",
265 }
266}
267
268pub(crate) fn kind_label(kind: AvisoErrorKind) -> &'static str {
271 match kind {
272 AvisoErrorKind::Transport => "transport",
273 AvisoErrorKind::Http => "http",
274 AvisoErrorKind::Auth => "auth",
275 AvisoErrorKind::Decode => "decode",
276 AvisoErrorKind::MalformedEvent => "malformed_event",
277 AvisoErrorKind::HistoryGap => "history_gap",
278 AvisoErrorKind::StreamProtocol => "stream_protocol",
279 AvisoErrorKind::Config => "config",
280 AvisoErrorKind::StateStore => "state_store",
281 AvisoErrorKind::Trigger => "trigger",
282 AvisoErrorKind::InvalidInput => "invalid_input",
283 AvisoErrorKind::InvalidUsage => "invalid_usage",
284 AvisoErrorKind::Internal => "internal",
285 AvisoErrorKind::Panic => "panic",
286 AvisoErrorKind::Unknown => "unknown",
287 }
288}
289
290pub(crate) fn invalid_input(message: &str) -> OutcomeError {
291 OutcomeError::build(
292 AvisoErrorKind::InvalidInput,
293 0,
294 message.to_string(),
295 None,
296 None,
297 None,
298 )
299}
300
301pub(crate) fn invalid_usage(message: &str) -> OutcomeError {
302 OutcomeError::build(
303 AvisoErrorKind::InvalidUsage,
304 0,
305 message.to_string(),
306 None,
307 None,
308 None,
309 )
310}
311
312pub(crate) fn internal(message: &str) -> OutcomeError {
313 OutcomeError::build(
314 AvisoErrorKind::Internal,
315 0,
316 message.to_string(),
317 None,
318 None,
319 None,
320 )
321}
322
323pub(crate) fn panic_error() -> OutcomeError {
326 OutcomeError::build(
327 AvisoErrorKind::Panic,
328 0,
329 "a Rust panic was caught at the FFI boundary".to_string(),
330 None,
331 None,
332 None,
333 )
334}
335
336pub(crate) fn panic_outcome() -> *mut AvisoOutcome {
339 panic_error().into_outcome()
340}