cloudkit 0.1.0

Safe Rust bindings for Apple's CloudKit framework — iCloud databases and sync on macOS
Documentation
use core::ffi::{c_char, c_void};
use core::ptr;

use crate::database::{CKDatabase, CKDatabaseScope};
use crate::error::CloudKitError;
use crate::ffi;
use crate::private::{
    box_closure, error_from_status, opt_cstring_ptr, optional_cstring_from_str,
    parse_borrowed_error_ptr, parse_json_ptr, parse_json_str,
};
use crate::record::CKRecordID;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AccountStatus {
    CouldNotDetermine,
    Available,
    Restricted,
    NoAccount,
    TemporarilyUnavailable,
    Unknown(i32),
}

impl AccountStatus {
    pub(crate) const fn from_raw(raw: i32) -> Self {
        match raw {
            0 => Self::CouldNotDetermine,
            1 => Self::Available,
            2 => Self::Restricted,
            3 => Self::NoAccount,
            4 => Self::TemporarilyUnavailable,
            other => Self::Unknown(other),
        }
    }
}

impl core::fmt::Display for AccountStatus {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        let label = match self {
            Self::CouldNotDetermine => "couldNotDetermine",
            Self::Available => "available",
            Self::Restricted => "restricted",
            Self::NoAccount => "noAccount",
            Self::TemporarilyUnavailable => "temporarilyUnavailable",
            Self::Unknown(raw) => return write!(f, "unknown({raw})"),
        };
        f.write_str(label)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CKContainer {
    identifier: Option<String>,
}

impl Default for CKContainer {
    fn default() -> Self {
        Self::new()
    }
}

impl CKContainer {
    pub fn new() -> Self {
        Self { identifier: None }
    }

    pub fn default() -> Self {
        Self::new()
    }

    pub fn container(identifier: impl Into<String>) -> Self {
        Self {
            identifier: Some(identifier.into()),
        }
    }

    pub fn container_identifier(&self) -> Option<&str> {
        self.identifier.as_deref()
    }

    pub fn private_cloud_database(&self) -> CKDatabase {
        CKDatabase::new(self.clone(), CKDatabaseScope::Private)
    }

    pub fn public_cloud_database(&self) -> CKDatabase {
        CKDatabase::new(self.clone(), CKDatabaseScope::Public)
    }

    pub fn shared_cloud_database(&self) -> CKDatabase {
        CKDatabase::new(self.clone(), CKDatabaseScope::Shared)
    }

    pub fn account_status(&self) -> Result<AccountStatus, CloudKitError> {
        let identifier =
            optional_cstring_from_str(self.identifier.as_deref(), "container identifier")?;
        let mut out_status = 0_i32;
        let mut out_error: *mut c_char = ptr::null_mut();
        let status = unsafe {
            ffi::ck_container_account_status_sync(
                opt_cstring_ptr(&identifier),
                &mut out_status,
                &mut out_error,
            )
        };
        if status != ffi::status::OK {
            return Err(unsafe { error_from_status(status, out_error) });
        }
        Ok(AccountStatus::from_raw(out_status))
    }

    pub fn account_status_with_completion_handler<F>(
        &self,
        callback: F,
    ) -> Result<(), CloudKitError>
    where
        F: FnOnce(Result<AccountStatus, CloudKitError>) + Send + 'static,
    {
        let identifier =
            optional_cstring_from_str(self.identifier.as_deref(), "container identifier")?;
        let callback_ptr = box_closure(Box::new(callback) as AccountStatusCallback);
        unsafe {
            ffi::ck_container_account_status_async(
                opt_cstring_ptr(&identifier),
                account_status_trampoline,
                callback_ptr,
            );
        }
        Ok(())
    }

    pub fn fetch_user_record_id(&self) -> Result<CKRecordID, CloudKitError> {
        let identifier =
            optional_cstring_from_str(self.identifier.as_deref(), "container identifier")?;
        let mut out_json: *mut c_char = ptr::null_mut();
        let mut out_error: *mut c_char = ptr::null_mut();
        let status = unsafe {
            ffi::ck_container_fetch_user_record_id_sync(
                opt_cstring_ptr(&identifier),
                &mut out_json,
                &mut out_error,
            )
        };
        if status != ffi::status::OK {
            return Err(unsafe { error_from_status(status, out_error) });
        }
        let record_id = unsafe {
            parse_json_ptr::<crate::private::CKRecordIDPayload>(out_json, "user record ID")?
        };
        Ok(CKRecordID::from_payload(record_id))
    }

    pub fn fetch_user_record_id_with_completion_handler<F>(
        &self,
        callback: F,
    ) -> Result<(), CloudKitError>
    where
        F: FnOnce(Result<CKRecordID, CloudKitError>) + Send + 'static,
    {
        let identifier =
            optional_cstring_from_str(self.identifier.as_deref(), "container identifier")?;
        let callback_ptr = box_closure(Box::new(callback) as RecordIdCallback);
        unsafe {
            ffi::ck_container_fetch_user_record_id_async(
                opt_cstring_ptr(&identifier),
                record_id_trampoline,
                callback_ptr,
            );
        }
        Ok(())
    }
}

type AccountStatusCallback = Box<dyn FnOnce(Result<AccountStatus, CloudKitError>) + Send + 'static>;
type RecordIdCallback = Box<dyn FnOnce(Result<CKRecordID, CloudKitError>) + Send + 'static>;

unsafe extern "C" fn account_status_trampoline(
    refcon: *mut c_void,
    status_raw: i32,
    error_json: *const c_char,
) {
    let callback: Box<AccountStatusCallback> = Box::from_raw(refcon.cast());
    let result = if error_json.is_null() {
        Ok(AccountStatus::from_raw(status_raw))
    } else {
        Err(parse_borrowed_error_ptr(error_json))
    };
    callback(result);
}

unsafe extern "C" fn record_id_trampoline(
    refcon: *mut c_void,
    json: *const c_char,
    error_json: *const c_char,
) {
    let callback: Box<RecordIdCallback> = Box::from_raw(refcon.cast());
    let result = if error_json.is_null() {
        let payload = parse_json_str::<crate::private::CKRecordIDPayload>(
            &std::ffi::CStr::from_ptr(json).to_string_lossy(),
            "user record ID",
        );
        payload.map(CKRecordID::from_payload)
    } else {
        Err(parse_borrowed_error_ptr(error_json))
    };
    callback(result);
}