cloudkit 0.1.0

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

use crate::database::CKDatabase;
use crate::error::CloudKitError;
use crate::ffi;
use crate::private::{
    error_from_status, json_cstring, opt_cstring_ptr, optional_cstring_from_str, parse_json_ptr,
    CKModifyRecordsOperationPayload, CKModifyRecordsResultPayload, CKQueryOperationPayload,
    CKQueryOperationResultPayload,
};
use crate::query::CKQuery;
use crate::record::{CKRecord, CKRecordID, CKRecordZoneID};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CKRecordSavePolicy {
    IfServerRecordUnchanged = 0,
    ChangedKeys = 1,
    AllKeys = 2,
}

#[derive(Debug, Clone, PartialEq)]
pub struct CKRecordSaveResult {
    pub record_id: CKRecordID,
    pub record: Option<CKRecord>,
    pub error: Option<crate::error::CloudKitError>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct CKRecordDeleteResult {
    pub record_id: CKRecordID,
    pub error: Option<crate::error::CloudKitError>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct ModifyRecordsResult {
    pub saved_records: Vec<CKRecord>,
    pub deleted_record_ids: Vec<CKRecordID>,
    pub save_results: Vec<CKRecordSaveResult>,
    pub delete_results: Vec<CKRecordDeleteResult>,
    pub operation_error: Option<crate::error::CloudKitError>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct QueryMatchResult {
    pub record_id: CKRecordID,
    pub record: Option<CKRecord>,
    pub error: Option<crate::error::CloudKitError>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct QueryOperationResult {
    pub records: Vec<CKRecord>,
    pub matches: Vec<QueryMatchResult>,
    pub cursor_returned: bool,
    pub operation_error: Option<crate::error::CloudKitError>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct CKModifyRecordsOperation {
    records_to_save: Vec<CKRecord>,
    record_ids_to_delete: Vec<CKRecordID>,
    save_policy: CKRecordSavePolicy,
    atomic: bool,
}

impl CKModifyRecordsOperation {
    pub fn new(records_to_save: Vec<CKRecord>, record_ids_to_delete: Vec<CKRecordID>) -> Self {
        Self {
            records_to_save,
            record_ids_to_delete,
            save_policy: CKRecordSavePolicy::IfServerRecordUnchanged,
            atomic: true,
        }
    }

    pub fn records_to_save(&self) -> &[CKRecord] {
        &self.records_to_save
    }

    pub fn record_ids_to_delete(&self) -> &[CKRecordID] {
        &self.record_ids_to_delete
    }

    pub const fn save_policy(&self) -> CKRecordSavePolicy {
        self.save_policy
    }

    pub const fn atomic(&self) -> bool {
        self.atomic
    }

    pub fn with_save_policy(mut self, save_policy: CKRecordSavePolicy) -> Self {
        self.save_policy = save_policy;
        self
    }

    pub fn with_atomic(mut self, atomic: bool) -> Self {
        self.atomic = atomic;
        self
    }

    pub fn execute_in(&self, database: &CKDatabase) -> Result<ModifyRecordsResult, CloudKitError> {
        let identifier = optional_cstring_from_str(
            database.container().container_identifier(),
            "container identifier",
        )?;
        let payload = CKModifyRecordsOperationPayload {
            records_to_save: self
                .records_to_save
                .iter()
                .map(CKRecord::to_payload)
                .collect(),
            record_ids_to_delete: self
                .record_ids_to_delete
                .iter()
                .map(CKRecordID::to_payload)
                .collect(),
            save_policy: self.save_policy as i32,
            atomic: self.atomic,
        };
        let operation_json = json_cstring(&payload, "modify-records operation")?;
        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_execute_modify_records_sync(
                opt_cstring_ptr(&identifier),
                database.database_scope() as i32,
                operation_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::<CKModifyRecordsResultPayload>(out_json, "modify-records result")?
        };
        Ok(ModifyRecordsResult {
            saved_records: payload
                .saved_records
                .into_iter()
                .map(CKRecord::from_payload)
                .collect(),
            deleted_record_ids: payload
                .deleted_record_ids
                .into_iter()
                .map(CKRecordID::from_payload)
                .collect(),
            save_results: payload
                .save_results
                .into_iter()
                .map(|entry| CKRecordSaveResult {
                    record_id: CKRecordID::from_payload(entry.record_id),
                    record: entry.record.map(CKRecord::from_payload),
                    error: entry.error.map(crate::error::CloudKitError::from_payload),
                })
                .collect(),
            delete_results: payload
                .delete_results
                .into_iter()
                .map(|entry| CKRecordDeleteResult {
                    record_id: CKRecordID::from_payload(entry.record_id),
                    error: entry.error.map(crate::error::CloudKitError::from_payload),
                })
                .collect(),
            operation_error: payload
                .operation_error
                .map(crate::error::CloudKitError::from_payload),
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CKQueryOperation {
    query: CKQuery,
    zone_id: Option<CKRecordZoneID>,
    desired_keys: Option<Vec<String>>,
    results_limit: Option<usize>,
}

impl CKQueryOperation {
    pub fn new(query: CKQuery) -> Self {
        Self {
            query,
            zone_id: None,
            desired_keys: None,
            results_limit: None,
        }
    }

    pub fn query(&self) -> &CKQuery {
        &self.query
    }

    pub const fn zone_id(&self) -> Option<&CKRecordZoneID> {
        self.zone_id.as_ref()
    }

    pub fn desired_keys(&self) -> Option<&[String]> {
        self.desired_keys.as_deref()
    }

    pub const fn results_limit(&self) -> Option<usize> {
        self.results_limit
    }

    pub fn with_zone_id(mut self, zone_id: CKRecordZoneID) -> Self {
        self.zone_id = Some(zone_id);
        self
    }

    pub fn with_desired_keys(mut self, desired_keys: Vec<String>) -> Self {
        self.desired_keys = Some(desired_keys);
        self
    }

    pub fn with_results_limit(mut self, results_limit: usize) -> Self {
        self.results_limit = Some(results_limit);
        self
    }

    pub fn execute_in(&self, database: &CKDatabase) -> Result<QueryOperationResult, CloudKitError> {
        let identifier = optional_cstring_from_str(
            database.container().container_identifier(),
            "container identifier",
        )?;
        let payload = CKQueryOperationPayload {
            query: self.query.to_payload(),
            zone_id: self.zone_id.as_ref().map(CKRecordZoneID::to_payload),
            desired_keys: self.desired_keys.clone(),
            results_limit: self.results_limit,
        };
        let operation_json = json_cstring(&payload, "query operation")?;
        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_execute_query_operation_sync(
                opt_cstring_ptr(&identifier),
                database.database_scope() as i32,
                operation_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::<CKQueryOperationResultPayload>(out_json, "query operation result")?
        };
        Ok(QueryOperationResult {
            records: payload
                .records
                .into_iter()
                .map(CKRecord::from_payload)
                .collect(),
            matches: payload
                .matches
                .into_iter()
                .map(|entry| QueryMatchResult {
                    record_id: CKRecordID::from_payload(entry.record_id),
                    record: entry.record.map(CKRecord::from_payload),
                    error: entry.error.map(crate::error::CloudKitError::from_payload),
                })
                .collect(),
            cursor_returned: payload.cursor_returned,
            operation_error: payload
                .operation_error
                .map(crate::error::CloudKitError::from_payload),
        })
    }
}