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::container::CKContainer;
use crate::error::CloudKitError;
use crate::ffi;
use crate::private::{
    box_closure, error_from_status, json_cstring, opt_cstring_ptr, optional_cstring_from_str,
    parse_borrowed_error_ptr, parse_json_ptr, parse_json_str,
};
use crate::query::CKQuery;
use crate::record::{CKRecord, CKRecordID, CKRecordZoneID};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CKDatabaseScope {
    Public = 1,
    Private = 2,
    Shared = 3,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CKDatabase {
    container: CKContainer,
    database_scope: CKDatabaseScope,
}

impl CKDatabase {
    pub(crate) const fn new(container: CKContainer, database_scope: CKDatabaseScope) -> Self {
        Self {
            container,
            database_scope,
        }
    }

    pub const fn database_scope(&self) -> CKDatabaseScope {
        self.database_scope
    }

    pub const fn container(&self) -> &CKContainer {
        &self.container
    }

    pub fn save_record(&self, record: &CKRecord) -> Result<CKRecord, CloudKitError> {
        let identifier = optional_cstring_from_str(
            self.container.container_identifier(),
            "container identifier",
        )?;
        let record_json = json_cstring(&record.to_payload(), "record")?;
        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_database_save_record_sync(
                opt_cstring_ptr(&identifier),
                self.database_scope as i32,
                record_json.as_ptr(),
                &mut out_json,
                &mut out_error,
            )
        };
        if status != ffi::status::OK {
            return Err(unsafe { error_from_status(status, out_error) });
        }
        let payload =
            unsafe { parse_json_ptr::<crate::private::CKRecordPayload>(out_json, "saved record")? };
        Ok(CKRecord::from_payload(payload))
    }

    pub fn fetch_record(&self, record_id: &CKRecordID) -> Result<CKRecord, CloudKitError> {
        let identifier = optional_cstring_from_str(
            self.container.container_identifier(),
            "container identifier",
        )?;
        let record_id_json = json_cstring(&record_id.to_payload(), "record ID")?;
        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_database_fetch_record_sync(
                opt_cstring_ptr(&identifier),
                self.database_scope as i32,
                record_id_json.as_ptr(),
                &mut out_json,
                &mut out_error,
            )
        };
        if status != ffi::status::OK {
            return Err(unsafe { error_from_status(status, out_error) });
        }
        let payload = unsafe {
            parse_json_ptr::<crate::private::CKRecordPayload>(out_json, "fetched record")?
        };
        Ok(CKRecord::from_payload(payload))
    }

    pub fn delete_record(&self, record_id: &CKRecordID) -> Result<CKRecordID, CloudKitError> {
        let identifier = optional_cstring_from_str(
            self.container.container_identifier(),
            "container identifier",
        )?;
        let record_id_json = json_cstring(&record_id.to_payload(), "record ID")?;
        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_database_delete_record_sync(
                opt_cstring_ptr(&identifier),
                self.database_scope as i32,
                record_id_json.as_ptr(),
                &mut out_json,
                &mut out_error,
            )
        };
        if status != ffi::status::OK {
            return Err(unsafe { error_from_status(status, out_error) });
        }
        let payload = unsafe {
            parse_json_ptr::<crate::private::CKRecordIDPayload>(out_json, "deleted record ID")?
        };
        Ok(CKRecordID::from_payload(payload))
    }

    pub fn perform_query(
        &self,
        query: &CKQuery,
        zone_id: Option<&CKRecordZoneID>,
    ) -> Result<Vec<CKRecord>, CloudKitError> {
        let identifier = optional_cstring_from_str(
            self.container.container_identifier(),
            "container identifier",
        )?;
        let query_json = json_cstring(&query.to_payload(), "query")?;
        let zone_json = zone_id.map(CKRecordZoneID::to_payload);
        let zone_json = match zone_json.as_ref() {
            Some(zone) => Some(json_cstring(zone, "zone ID")?),
            None => None,
        };
        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_database_perform_query_sync(
                opt_cstring_ptr(&identifier),
                self.database_scope as i32,
                query_json.as_ptr(),
                opt_cstring_ptr(&zone_json),
                &mut out_json,
                &mut out_error,
            )
        };
        if status != ffi::status::OK {
            return Err(unsafe { error_from_status(status, out_error) });
        }
        let payloads = unsafe {
            parse_json_ptr::<Vec<crate::private::CKRecordPayload>>(out_json, "query results")?
        };
        Ok(payloads.into_iter().map(CKRecord::from_payload).collect())
    }

    pub fn perform_query_with_completion_handler<F>(
        &self,
        query: &CKQuery,
        zone_id: Option<&CKRecordZoneID>,
        callback: F,
    ) -> Result<(), CloudKitError>
    where
        F: FnOnce(Result<Vec<CKRecord>, CloudKitError>) + Send + 'static,
    {
        let identifier = optional_cstring_from_str(
            self.container.container_identifier(),
            "container identifier",
        )?;
        let query_json = json_cstring(&query.to_payload(), "query")?;
        let zone_json = zone_id.map(CKRecordZoneID::to_payload);
        let zone_json = match zone_json.as_ref() {
            Some(zone) => Some(json_cstring(zone, "zone ID")?),
            None => None,
        };
        let callback_ptr = box_closure(Box::new(callback) as QueryCallback);
        unsafe {
            ffi::ck_database_perform_query_async(
                opt_cstring_ptr(&identifier),
                self.database_scope as i32,
                query_json.as_ptr(),
                opt_cstring_ptr(&zone_json),
                query_callback_trampoline,
                callback_ptr,
            );
        }
        Ok(())
    }
}

type QueryCallback = Box<dyn FnOnce(Result<Vec<CKRecord>, CloudKitError>) + Send + 'static>;

unsafe extern "C" fn query_callback_trampoline(
    refcon: *mut c_void,
    json: *const c_char,
    error_json: *const c_char,
) {
    let callback: Box<QueryCallback> = Box::from_raw(refcon.cast());
    let result = if error_json.is_null() {
        let payloads = parse_json_str::<Vec<crate::private::CKRecordPayload>>(
            &std::ffi::CStr::from_ptr(json).to_string_lossy(),
            "query results",
        );
        payloads.map(|payloads| payloads.into_iter().map(CKRecord::from_payload).collect())
    } else {
        Err(parse_borrowed_error_ptr(error_json))
    };
    callback(result);
}