Skip to main content

cloudkit/
error.rs

1use core::fmt;
2
3use serde::{Deserialize, Serialize};
4
5/// Mirrors `CKErrorDomain`.
6pub const CLOUDKIT_ERROR_DOMAIN: &str = "CKErrorDomain";
7/// Mirrors `CloudKitBridge`.
8pub const CLOUDKIT_BRIDGE_ERROR_DOMAIN: &str = "CloudKitBridge";
9
10/// Mirrors `CKError.Code`.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub enum CloudKitErrorCode {
14    /// Mirrors `CKError.Code.bridgeInvalidArgument`.
15    BridgeInvalidArgument,
16    /// Mirrors `CKError.Code.bridgeFailure`.
17    BridgeFailure,
18    /// Mirrors `CKError.Code.bridgeTimedOut`.
19    BridgeTimedOut,
20    /// Mirrors `CKError.Code.bridgeDefaultContainerUnavailable`.
21    BridgeDefaultContainerUnavailable,
22    /// Mirrors `CKError.Code.internalError`.
23    InternalError,
24    /// Mirrors `CKError.Code.partialFailure`.
25    PartialFailure,
26    /// Mirrors `CKError.Code.networkUnavailable`.
27    NetworkUnavailable,
28    /// Mirrors `CKError.Code.networkFailure`.
29    NetworkFailure,
30    /// Mirrors `CKError.Code.badContainer`.
31    BadContainer,
32    /// Mirrors `CKError.Code.serviceUnavailable`.
33    ServiceUnavailable,
34    /// Mirrors `CKError.Code.requestRateLimited`.
35    RequestRateLimited,
36    /// Mirrors `CKError.Code.missingEntitlement`.
37    MissingEntitlement,
38    /// Mirrors `CKError.Code.notAuthenticated`.
39    NotAuthenticated,
40    /// Mirrors `CKError.Code.permissionFailure`.
41    PermissionFailure,
42    /// Mirrors `CKError.Code.unknownItem`.
43    UnknownItem,
44    /// Mirrors `CKError.Code.invalidArguments`.
45    InvalidArguments,
46    /// Mirrors `CKError.Code.serverRecordChanged`.
47    ServerRecordChanged,
48    /// Mirrors `CKError.Code.operationCancelled`.
49    OperationCancelled,
50    /// Mirrors `CKError.Code.badDatabase`.
51    BadDatabase,
52    /// Mirrors `CKError.Code.zoneNotFound`.
53    ZoneNotFound,
54    /// Mirrors `CKError.Code.limitExceeded`.
55    LimitExceeded,
56    /// Mirrors `CKError.Code.unknown`.
57    Unknown(i64),
58}
59
60/// Wraps `CKError` details.
61#[derive(Debug, Clone, PartialEq)]
62pub struct CloudKitError {
63    /// Mirrors `CKError.domain`.
64    pub domain: String,
65    /// Mirrors `CKError.code`.
66    pub code: i64,
67    /// Mirrors `CKError.localizedDescription`.
68    pub message: String,
69    /// Mirrors `CKErrorRetryAfterKey`.
70    pub retry_after_seconds: Option<f64>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub(crate) struct ErrorPayload {
76    pub domain: String,
77    pub code: i64,
78    pub message: String,
79    pub retry_after_seconds: Option<f64>,
80}
81
82impl CloudKitError {
83    pub(crate) fn from_payload(payload: ErrorPayload) -> Self {
84        Self {
85            domain: payload.domain,
86            code: payload.code,
87            message: payload.message,
88            retry_after_seconds: payload.retry_after_seconds,
89        }
90    }
91
92    pub(crate) fn bridge(code: i64, message: impl Into<String>) -> Self {
93        Self {
94            domain: CLOUDKIT_BRIDGE_ERROR_DOMAIN.into(),
95            code,
96            message: message.into(),
97            retry_after_seconds: None,
98        }
99    }
100
101    /// Mirrors `CKError.code`.
102    #[must_use]
103    pub fn kind(&self) -> CloudKitErrorCode {
104        if self.domain == CLOUDKIT_BRIDGE_ERROR_DOMAIN {
105            return match self.code {
106                -1 => CloudKitErrorCode::BridgeInvalidArgument,
107                -2 => CloudKitErrorCode::BridgeFailure,
108                -3 => CloudKitErrorCode::BridgeTimedOut,
109                -4 => CloudKitErrorCode::BridgeDefaultContainerUnavailable,
110                other => CloudKitErrorCode::Unknown(other),
111            };
112        }
113
114        if self.domain != CLOUDKIT_ERROR_DOMAIN {
115            return CloudKitErrorCode::Unknown(self.code);
116        }
117
118        match self.code {
119            1 => CloudKitErrorCode::InternalError,
120            2 => CloudKitErrorCode::PartialFailure,
121            3 => CloudKitErrorCode::NetworkUnavailable,
122            4 => CloudKitErrorCode::NetworkFailure,
123            5 => CloudKitErrorCode::BadContainer,
124            6 => CloudKitErrorCode::ServiceUnavailable,
125            7 => CloudKitErrorCode::RequestRateLimited,
126            8 => CloudKitErrorCode::MissingEntitlement,
127            9 => CloudKitErrorCode::NotAuthenticated,
128            10 => CloudKitErrorCode::PermissionFailure,
129            11 => CloudKitErrorCode::UnknownItem,
130            12 => CloudKitErrorCode::InvalidArguments,
131            14 => CloudKitErrorCode::ServerRecordChanged,
132            20 => CloudKitErrorCode::OperationCancelled,
133            24 => CloudKitErrorCode::BadDatabase,
134            26 => CloudKitErrorCode::ZoneNotFound,
135            27 => CloudKitErrorCode::LimitExceeded,
136            other => CloudKitErrorCode::Unknown(other),
137        }
138    }
139
140    /// Reports whether the wrapped `CKError` matches this convenience condition.
141    #[must_use]
142    pub fn is_entitlement_or_account_issue(&self) -> bool {
143        matches!(
144            self.kind(),
145            CloudKitErrorCode::BridgeDefaultContainerUnavailable
146                | CloudKitErrorCode::BadContainer
147                | CloudKitErrorCode::MissingEntitlement
148                | CloudKitErrorCode::NotAuthenticated
149                | CloudKitErrorCode::PermissionFailure
150        )
151    }
152
153    /// Reports whether the wrapped `CKError` matches this convenience condition.
154    #[must_use]
155    pub fn is_retryable(&self) -> bool {
156        matches!(
157            self.kind(),
158            CloudKitErrorCode::NetworkUnavailable
159                | CloudKitErrorCode::NetworkFailure
160                | CloudKitErrorCode::ServiceUnavailable
161                | CloudKitErrorCode::RequestRateLimited
162        )
163    }
164
165    /// Reports whether the wrapped `CKError` matches this convenience condition.
166    #[must_use]
167    pub fn is_missing_entitlement(&self) -> bool {
168        matches!(self.kind(), CloudKitErrorCode::MissingEntitlement)
169    }
170
171    /// Reports whether the wrapped `CKError` matches this convenience condition.
172    #[must_use]
173    pub fn is_not_authenticated(&self) -> bool {
174        matches!(self.kind(), CloudKitErrorCode::NotAuthenticated)
175    }
176}
177
178impl fmt::Display for CloudKitError {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        write!(f, "{} ({}) [{}]", self.message, self.code, self.domain)
181    }
182}
183
184impl std::error::Error for CloudKitError {}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn bridge_kind_maps_known_codes() {
192        let cases = [
193            (-1, CloudKitErrorCode::BridgeInvalidArgument),
194            (-2, CloudKitErrorCode::BridgeFailure),
195            (-3, CloudKitErrorCode::BridgeTimedOut),
196            (-4, CloudKitErrorCode::BridgeDefaultContainerUnavailable),
197        ];
198
199        for (code, expected) in cases {
200            let error = CloudKitError::bridge(code, "bridge failure");
201            assert_eq!(error.kind(), expected);
202        }
203    }
204
205    #[test]
206    fn framework_kind_maps_known_codes() {
207        let cases = [
208            (1, CloudKitErrorCode::InternalError),
209            (8, CloudKitErrorCode::MissingEntitlement),
210            (9, CloudKitErrorCode::NotAuthenticated),
211            (27, CloudKitErrorCode::LimitExceeded),
212        ];
213
214        for (code, expected) in cases {
215            let error = CloudKitError {
216                domain: CLOUDKIT_ERROR_DOMAIN.into(),
217                code,
218                message: "framework failure".into(),
219                retry_after_seconds: Some(1.5),
220            };
221            assert_eq!(error.kind(), expected);
222        }
223    }
224
225    #[test]
226    fn unknown_domain_and_codes_stay_unknown() {
227        let foreign_error = CloudKitError {
228            domain: "OtherDomain".into(),
229            code: 999,
230            message: "foreign".into(),
231            retry_after_seconds: None,
232        };
233        let bridge_error = CloudKitError::bridge(-99, "unknown bridge code");
234
235        assert_eq!(foreign_error.kind(), CloudKitErrorCode::Unknown(999));
236        assert_eq!(bridge_error.kind(), CloudKitErrorCode::Unknown(-99));
237    }
238
239    #[test]
240    fn convenience_predicates_match_expected_kinds() {
241        let missing_entitlement = CloudKitError {
242            domain: CLOUDKIT_ERROR_DOMAIN.into(),
243            code: 8,
244            message: "missing entitlement".into(),
245            retry_after_seconds: None,
246        };
247        let not_authenticated = CloudKitError {
248            domain: CLOUDKIT_ERROR_DOMAIN.into(),
249            code: 9,
250            message: "not authenticated".into(),
251            retry_after_seconds: None,
252        };
253        let retryable = CloudKitError {
254            domain: CLOUDKIT_ERROR_DOMAIN.into(),
255            code: 7,
256            message: "slow down".into(),
257            retry_after_seconds: Some(2.5),
258        };
259
260        assert!(missing_entitlement.is_entitlement_or_account_issue());
261        assert!(missing_entitlement.is_missing_entitlement());
262        assert!(!missing_entitlement.is_not_authenticated());
263        assert!(not_authenticated.is_entitlement_or_account_issue());
264        assert!(not_authenticated.is_not_authenticated());
265        assert!(retryable.is_retryable());
266    }
267
268    #[test]
269    fn from_payload_and_display_preserve_fields() {
270        let error = CloudKitError::from_payload(ErrorPayload {
271            domain: CLOUDKIT_ERROR_DOMAIN.into(),
272            code: 7,
273            message: "retry later".into(),
274            retry_after_seconds: Some(2.5),
275        });
276
277        assert_eq!(error.retry_after_seconds, Some(2.5));
278        assert_eq!(error.to_string(), "retry later (7) [CKErrorDomain]");
279    }
280}