1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3pub enum ErrorKind {
4 NetworkUnavailable,
6 Timeout,
7 RateLimited,
8 ServerError,
9 InvalidApiKey,
11 Forbidden,
12 NotFound,
13 InvalidRequest,
14 Conflict,
15 InvalidArgument,
16 InsufficientCredits,
18 FunnelTokenNotFound,
19 FunnelTokenExpired,
20 FunnelTokenAlreadyClaimed,
21 FunnelConfigUnsupported,
25 FunnelInvalid,
27 SubscriberUnresolvable,
32 PurchaseCanceled,
34 ProductNotAvailable,
35 AlreadyOwned,
36 PaymentDeclined,
37 StoreServiceUnavailable,
38 Ineligible,
39 ReceiptInvalid,
40 StoreProblem,
41 Storage,
43 Internal,
44}
45
46impl ErrorKind {
47 pub fn is_retryable(&self) -> bool {
48 matches!(
49 self,
50 ErrorKind::NetworkUnavailable
51 | ErrorKind::Timeout
52 | ErrorKind::RateLimited
53 | ErrorKind::ServerError
54 | ErrorKind::StoreServiceUnavailable
55 )
56 }
57
58 pub fn default_message(&self) -> &'static str {
60 match self {
61 ErrorKind::NetworkUnavailable => "network unavailable",
62 ErrorKind::Timeout => "request timed out",
63 ErrorKind::RateLimited => "rate limited",
64 ErrorKind::ServerError => "server error",
65 ErrorKind::InvalidApiKey => "invalid api key",
66 ErrorKind::Forbidden => "forbidden",
67 ErrorKind::NotFound => "not found",
68 ErrorKind::InvalidRequest => "invalid request",
69 ErrorKind::Conflict => "conflict",
70 ErrorKind::InvalidArgument => "invalid argument",
71 ErrorKind::InsufficientCredits => "insufficient credits",
72 ErrorKind::FunnelTokenNotFound => "funnel token not found",
73 ErrorKind::FunnelTokenExpired => "funnel token expired",
74 ErrorKind::FunnelTokenAlreadyClaimed => "funnel token already claimed",
75 ErrorKind::FunnelConfigUnsupported => "funnel config format is newer than this SDK",
76 ErrorKind::FunnelInvalid => "funnel config is invalid",
77 ErrorKind::SubscriberUnresolvable => "this device's subscriber cannot be resolved",
78 ErrorKind::PurchaseCanceled => "purchase canceled",
79 ErrorKind::ProductNotAvailable => "product not available",
80 ErrorKind::AlreadyOwned => "already owned",
81 ErrorKind::PaymentDeclined => "payment declined",
82 ErrorKind::StoreServiceUnavailable => "store service unavailable",
83 ErrorKind::Ineligible => "ineligible",
84 ErrorKind::ReceiptInvalid => "receipt invalid",
85 ErrorKind::StoreProblem => "store problem",
86 ErrorKind::Storage => "storage error",
87 ErrorKind::Internal => "internal error",
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct RovenueError {
95 pub kind: ErrorKind,
96 pub message: String,
97 pub server_code: Option<String>,
98 pub http_status: Option<u16>,
99 pub retryable: bool,
100}
101
102impl RovenueError {
103 pub fn kind(kind: ErrorKind) -> Self {
105 Self {
106 kind,
107 message: kind.default_message().to_string(),
108 server_code: None,
109 http_status: None,
110 retryable: kind.is_retryable(),
111 }
112 }
113 pub fn http(
115 kind: ErrorKind,
116 status: u16,
117 server_code: Option<String>,
118 message: String,
119 ) -> Self {
120 let message = if message.is_empty() {
121 kind.default_message().to_string()
122 } else {
123 message
124 };
125 Self {
126 kind,
127 message,
128 server_code,
129 http_status: Some(status),
130 retryable: kind.is_retryable(),
131 }
132 }
133
134 pub fn is_poison(&self) -> bool {
155 !self.retryable && matches!(self.http_status, Some(400) | Some(422))
156 }
157}
158
159impl RovenueError {
160 #[allow(non_snake_case)]
165 pub fn Internal() -> Self {
166 Self::kind(ErrorKind::Internal)
167 }
168 #[allow(non_snake_case)]
169 pub fn NetworkUnavailable() -> Self {
170 Self::kind(ErrorKind::NetworkUnavailable)
171 }
172 #[allow(non_snake_case)]
173 pub fn ServerError() -> Self {
174 Self::kind(ErrorKind::ServerError)
175 }
176 #[allow(non_snake_case)]
177 pub fn RateLimited() -> Self {
178 Self::kind(ErrorKind::RateLimited)
179 }
180 #[allow(non_snake_case)]
181 pub fn InvalidApiKey() -> Self {
182 Self::kind(ErrorKind::InvalidApiKey)
183 }
184 #[allow(non_snake_case)]
185 pub fn Timeout() -> Self {
186 Self::kind(ErrorKind::Timeout)
187 }
188 #[allow(non_snake_case)]
189 pub fn InsufficientCredits() -> Self {
190 Self::kind(ErrorKind::InsufficientCredits)
191 }
192 #[allow(non_snake_case)]
193 pub fn Storage() -> Self {
194 Self::kind(ErrorKind::Storage)
195 }
196 #[allow(non_snake_case)]
197 pub fn InvalidArgument() -> Self {
198 Self::kind(ErrorKind::InvalidArgument)
199 }
200 #[allow(non_snake_case)]
201 pub fn FunnelConfigUnsupported() -> Self {
202 Self::kind(ErrorKind::FunnelConfigUnsupported)
203 }
204 #[allow(non_snake_case)]
205 pub fn FunnelInvalid() -> Self {
206 Self::kind(ErrorKind::FunnelInvalid)
207 }
208 #[allow(non_snake_case)]
209 pub fn FunnelTokenNotFound() -> Self {
210 Self::kind(ErrorKind::FunnelTokenNotFound)
211 }
212 #[allow(non_snake_case)]
213 pub fn FunnelTokenExpired() -> Self {
214 Self::kind(ErrorKind::FunnelTokenExpired)
215 }
216 #[allow(non_snake_case)]
217 pub fn FunnelTokenAlreadyClaimed() -> Self {
218 Self::kind(ErrorKind::FunnelTokenAlreadyClaimed)
219 }
220}
221
222impl std::fmt::Display for RovenueError {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 write!(f, "{}", self.message)
225 }
226}
227
228impl std::error::Error for RovenueError {}
229
230pub type RovenueResult<T> = std::result::Result<T, RovenueError>;
231
232#[derive(Debug, Clone)]
247pub enum RovenueErrorFfi {
248 Generic {
249 kind: ErrorKind,
250 detail: String,
254 server_code: Option<String>,
255 http_status: Option<u16>,
256 retryable: bool,
257 },
258}
259
260impl From<RovenueError> for RovenueErrorFfi {
261 fn from(e: RovenueError) -> Self {
262 RovenueErrorFfi::Generic {
263 kind: e.kind,
264 detail: e.message,
265 server_code: e.server_code,
266 http_status: e.http_status,
267 retryable: e.retryable,
268 }
269 }
270}
271
272impl std::fmt::Display for RovenueErrorFfi {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 let RovenueErrorFfi::Generic { detail, .. } = self;
275 write!(f, "{detail}")
276 }
277}
278
279impl std::error::Error for RovenueErrorFfi {}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn retryable_kinds_are_classified() {
287 for k in [
288 ErrorKind::NetworkUnavailable,
289 ErrorKind::Timeout,
290 ErrorKind::RateLimited,
291 ErrorKind::ServerError,
292 ErrorKind::StoreServiceUnavailable,
293 ] {
294 assert!(k.is_retryable(), "{k:?} should be retryable");
295 }
296 for k in [
297 ErrorKind::InvalidApiKey,
298 ErrorKind::Forbidden,
299 ErrorKind::NotFound,
300 ErrorKind::InvalidRequest,
301 ErrorKind::InsufficientCredits,
302 ErrorKind::Internal,
303 ] {
304 assert!(!k.is_retryable(), "{k:?} should NOT be retryable");
305 }
306 }
307
308 #[test]
309 fn http_constructor_carries_fields() {
310 let e = RovenueError::http(
311 ErrorKind::Forbidden,
312 403,
313 Some("FORBIDDEN".into()),
314 "no access".into(),
315 );
316 assert_eq!(e.kind, ErrorKind::Forbidden);
317 assert_eq!(e.http_status, Some(403));
318 assert_eq!(e.server_code.as_deref(), Some("FORBIDDEN"));
319 assert_eq!(e.message, "no access");
320 assert!(!e.retryable);
321 }
322
323 #[test]
324 fn kind_constructor_uses_default_message_and_no_http() {
325 let e = RovenueError::kind(ErrorKind::Timeout);
326 assert_eq!(e.kind, ErrorKind::Timeout);
327 assert_eq!(e.http_status, None);
328 assert_eq!(e.server_code, None);
329 assert!(e.retryable);
330 assert!(!e.message.is_empty());
331 }
332}