cloudkit 0.3.2

Safe Rust bindings for Apple's CloudKit framework — iCloud databases and sync on macOS
Documentation
use core::fmt;

use serde::{Deserialize, Serialize};

pub const CLOUDKIT_ERROR_DOMAIN: &str = "CKErrorDomain";
pub const CLOUDKIT_BRIDGE_ERROR_DOMAIN: &str = "CloudKitBridge";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CloudKitErrorCode {
    BridgeInvalidArgument,
    BridgeFailure,
    BridgeTimedOut,
    BridgeDefaultContainerUnavailable,
    InternalError,
    PartialFailure,
    NetworkUnavailable,
    NetworkFailure,
    BadContainer,
    ServiceUnavailable,
    RequestRateLimited,
    MissingEntitlement,
    NotAuthenticated,
    PermissionFailure,
    UnknownItem,
    InvalidArguments,
    ServerRecordChanged,
    OperationCancelled,
    BadDatabase,
    ZoneNotFound,
    LimitExceeded,
    Unknown(i64),
}

#[derive(Debug, Clone, PartialEq)]
pub struct CloudKitError {
    pub domain: String,
    pub code: i64,
    pub message: String,
    pub retry_after_seconds: Option<f64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ErrorPayload {
    pub domain: String,
    pub code: i64,
    pub message: String,
    pub retry_after_seconds: Option<f64>,
}

impl CloudKitError {
    pub(crate) fn from_payload(payload: ErrorPayload) -> Self {
        Self {
            domain: payload.domain,
            code: payload.code,
            message: payload.message,
            retry_after_seconds: payload.retry_after_seconds,
        }
    }

    pub(crate) fn bridge(code: i64, message: impl Into<String>) -> Self {
        Self {
            domain: CLOUDKIT_BRIDGE_ERROR_DOMAIN.into(),
            code,
            message: message.into(),
            retry_after_seconds: None,
        }
    }

    #[must_use]
    pub fn kind(&self) -> CloudKitErrorCode {
        if self.domain == CLOUDKIT_BRIDGE_ERROR_DOMAIN {
            return match self.code {
                -1 => CloudKitErrorCode::BridgeInvalidArgument,
                -2 => CloudKitErrorCode::BridgeFailure,
                -3 => CloudKitErrorCode::BridgeTimedOut,
                -4 => CloudKitErrorCode::BridgeDefaultContainerUnavailable,
                other => CloudKitErrorCode::Unknown(other),
            };
        }

        if self.domain != CLOUDKIT_ERROR_DOMAIN {
            return CloudKitErrorCode::Unknown(self.code);
        }

        match self.code {
            1 => CloudKitErrorCode::InternalError,
            2 => CloudKitErrorCode::PartialFailure,
            3 => CloudKitErrorCode::NetworkUnavailable,
            4 => CloudKitErrorCode::NetworkFailure,
            5 => CloudKitErrorCode::BadContainer,
            6 => CloudKitErrorCode::ServiceUnavailable,
            7 => CloudKitErrorCode::RequestRateLimited,
            8 => CloudKitErrorCode::MissingEntitlement,
            9 => CloudKitErrorCode::NotAuthenticated,
            10 => CloudKitErrorCode::PermissionFailure,
            11 => CloudKitErrorCode::UnknownItem,
            12 => CloudKitErrorCode::InvalidArguments,
            14 => CloudKitErrorCode::ServerRecordChanged,
            20 => CloudKitErrorCode::OperationCancelled,
            24 => CloudKitErrorCode::BadDatabase,
            26 => CloudKitErrorCode::ZoneNotFound,
            27 => CloudKitErrorCode::LimitExceeded,
            other => CloudKitErrorCode::Unknown(other),
        }
    }

    #[must_use]
    pub fn is_entitlement_or_account_issue(&self) -> bool {
        matches!(
            self.kind(),
            CloudKitErrorCode::BridgeDefaultContainerUnavailable
                | CloudKitErrorCode::BadContainer
                | CloudKitErrorCode::MissingEntitlement
                | CloudKitErrorCode::NotAuthenticated
                | CloudKitErrorCode::PermissionFailure
        )
    }

    #[must_use]
    pub fn is_retryable(&self) -> bool {
        matches!(
            self.kind(),
            CloudKitErrorCode::NetworkUnavailable
                | CloudKitErrorCode::NetworkFailure
                | CloudKitErrorCode::ServiceUnavailable
                | CloudKitErrorCode::RequestRateLimited
        )
    }

    #[must_use]
    pub fn is_missing_entitlement(&self) -> bool {
        matches!(self.kind(), CloudKitErrorCode::MissingEntitlement)
    }

    #[must_use]
    pub fn is_not_authenticated(&self) -> bool {
        matches!(self.kind(), CloudKitErrorCode::NotAuthenticated)
    }
}

impl fmt::Display for CloudKitError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} ({}) [{}]", self.message, self.code, self.domain)
    }
}

impl std::error::Error for CloudKitError {}