cloudkit 0.2.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::error::CloudKitError;
use crate::ffi;
use crate::private::{error_from_status, json_cstring, parse_json_ptr, CKShareParticipantPayload, CKSharePayload};
use crate::record::{CKRecord, CKRecordID, CKRecordZoneID};
use crate::user_identity::CKUserIdentity;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CKShareParticipantAcceptanceStatus {
    Unknown,
    Pending,
    Accepted,
    Removed,
    UnknownValue(i32),
}

impl CKShareParticipantAcceptanceStatus {
    const fn from_raw(raw: i32) -> Self {
        match raw {
            0 => Self::Unknown,
            1 => Self::Pending,
            2 => Self::Accepted,
            3 => Self::Removed,
            other => Self::UnknownValue(other),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CKShareParticipantPermission {
    Unknown,
    None,
    ReadOnly,
    ReadWrite,
    UnknownValue(i32),
}

impl CKShareParticipantPermission {
    pub(crate) const fn from_raw(raw: i32) -> Self {
        match raw {
            0 => Self::Unknown,
            1 => Self::None,
            2 => Self::ReadOnly,
            3 => Self::ReadWrite,
            other => Self::UnknownValue(other),
        }
    }

    pub(crate) const fn to_raw(self) -> i32 {
        match self {
            Self::Unknown => 0,
            Self::None => 1,
            Self::ReadOnly => 2,
            Self::ReadWrite => 3,
            Self::UnknownValue(raw) => raw,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CKShareParticipantRole {
    Unknown,
    Owner,
    Administrator,
    PrivateUser,
    PublicUser,
    UnknownValue(i32),
}

impl CKShareParticipantRole {
    const fn from_raw(raw: i32) -> Self {
        match raw {
            0 => Self::Unknown,
            1 => Self::Owner,
            2 => Self::Administrator,
            3 => Self::PrivateUser,
            4 => Self::PublicUser,
            other => Self::UnknownValue(other),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CKShareParticipant {
    archived_data: Vec<u8>,
    user_identity: CKUserIdentity,
    role: Option<CKShareParticipantRole>,
    permission: CKShareParticipantPermission,
    acceptance_status: CKShareParticipantAcceptanceStatus,
    participant_id: String,
    is_approved_requester: Option<bool>,
    date_added_to_share: Option<String>,
}

impl CKShareParticipant {
    pub fn one_time_url_participant() -> Result<Self, CloudKitError> {
        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_share_create_one_time_url_participant(&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::<CKShareParticipantPayload>(out_json, "share participant")? };
        Ok(Self::from_payload(payload))
    }

    pub fn archived_data(&self) -> &[u8] {
        &self.archived_data
    }

    pub fn user_identity(&self) -> &CKUserIdentity {
        &self.user_identity
    }

    pub const fn role(&self) -> Option<CKShareParticipantRole> {
        self.role
    }

    pub const fn permission(&self) -> CKShareParticipantPermission {
        self.permission
    }

    pub const fn acceptance_status(&self) -> CKShareParticipantAcceptanceStatus {
        self.acceptance_status
    }

    pub fn participant_id(&self) -> &str {
        &self.participant_id
    }

    pub const fn is_approved_requester(&self) -> Option<bool> {
        self.is_approved_requester
    }

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

    pub(crate) fn from_payload(payload: CKShareParticipantPayload) -> Self {
        Self {
            archived_data: payload.archived_data,
            user_identity: CKUserIdentity::from_payload(payload.user_identity),
            role: payload.role.map(CKShareParticipantRole::from_raw),
            permission: CKShareParticipantPermission::from_raw(payload.permission),
            acceptance_status: CKShareParticipantAcceptanceStatus::from_raw(payload.acceptance_status),
            participant_id: payload.participant_id,
            is_approved_requester: payload.is_approved_requester,
            date_added_to_share: payload.date_added_to_share,
        }
    }

    pub(crate) fn to_payload(&self) -> CKShareParticipantPayload {
        CKShareParticipantPayload {
            archived_data: self.archived_data.clone(),
            user_identity: crate::private::CKUserIdentityPayload {
                archived_data: self.user_identity.archived_data().to_vec(),
                user_record_id: self.user_identity.user_record_id().map(CKRecordID::to_payload),
                lookup_info: self
                    .user_identity
                    .lookup_info()
                    .map(crate::user_identity::CKUserIdentityLookupInfo::to_payload),
                name_components: self
                    .user_identity
                    .name_components()
                    .map(crate::user_identity::CKPersonNameComponents::to_payload),
                hasi_cloud_account: self.user_identity.has_i_cloud_account(),
                contact_identifiers: self.user_identity.contact_identifiers().to_vec(),
            },
            role: self.role.map(|role| match role {
                CKShareParticipantRole::Unknown => 0,
                CKShareParticipantRole::Owner => 1,
                CKShareParticipantRole::Administrator => 2,
                CKShareParticipantRole::PrivateUser => 3,
                CKShareParticipantRole::PublicUser => 4,
                CKShareParticipantRole::UnknownValue(raw) => raw,
            }),
            permission: self.permission.to_raw(),
            acceptance_status: match self.acceptance_status {
                CKShareParticipantAcceptanceStatus::Unknown => 0,
                CKShareParticipantAcceptanceStatus::Pending => 1,
                CKShareParticipantAcceptanceStatus::Accepted => 2,
                CKShareParticipantAcceptanceStatus::Removed => 3,
                CKShareParticipantAcceptanceStatus::UnknownValue(raw) => raw,
            },
            participant_id: self.participant_id.clone(),
            is_approved_requester: self.is_approved_requester,
            date_added_to_share: self.date_added_to_share.clone(),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct CKShare {
    share_record: CKRecord,
    root_record: Option<CKRecord>,
    zone_id: CKRecordZoneID,
    public_permission: CKShareParticipantPermission,
    url: Option<String>,
    participants: Vec<CKShareParticipant>,
    owner: Option<CKShareParticipant>,
    current_user_participant: Option<CKShareParticipant>,
    title: Option<String>,
    thumbnail_image_data: Option<Vec<u8>>,
    share_type: Option<String>,
    allows_access_requests: Option<bool>,
    is_zone_wide: bool,
}

impl CKShare {
    pub fn new_root_record(root_record: &CKRecord) -> Result<Self, CloudKitError> {
        let root_record_json = json_cstring(&root_record.to_payload(), "root 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_share_create_root_record(root_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::<CKSharePayload>(out_json, "share")? };
        Ok(Self::from_payload(payload))
    }

    pub fn new_zone_wide(zone_id: CKRecordZoneID) -> Result<Self, CloudKitError> {
        let zone_json = json_cstring(&zone_id.to_payload(), "zone 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_share_create_zone_wide(zone_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::<CKSharePayload>(out_json, "share")? };
        Ok(Self::from_payload(payload))
    }

    pub fn share_record(&self) -> &CKRecord {
        &self.share_record
    }

    pub const fn root_record(&self) -> Option<&CKRecord> {
        self.root_record.as_ref()
    }

    pub fn zone_id(&self) -> &CKRecordZoneID {
        &self.zone_id
    }

    pub const fn public_permission(&self) -> CKShareParticipantPermission {
        self.public_permission
    }

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

    pub fn participants(&self) -> &[CKShareParticipant] {
        &self.participants
    }

    pub const fn owner(&self) -> Option<&CKShareParticipant> {
        self.owner.as_ref()
    }

    pub const fn current_user_participant(&self) -> Option<&CKShareParticipant> {
        self.current_user_participant.as_ref()
    }

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

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

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

    pub const fn allows_access_requests(&self) -> Option<bool> {
        self.allows_access_requests
    }

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

    pub fn as_record(&self) -> CKRecord {
        self.share_record.clone()
    }

    pub fn with_public_permission(mut self, public_permission: CKShareParticipantPermission) -> Result<Self, CloudKitError> {
        self.public_permission = public_permission;
        self.normalize()
    }

    pub fn with_title(mut self, title: impl Into<String>) -> Result<Self, CloudKitError> {
        self.title = Some(title.into());
        self.normalize()
    }

    pub fn with_thumbnail_image_data(mut self, thumbnail_image_data: Vec<u8>) -> Result<Self, CloudKitError> {
        self.thumbnail_image_data = Some(thumbnail_image_data);
        self.normalize()
    }

    pub fn with_share_type(mut self, share_type: impl Into<String>) -> Result<Self, CloudKitError> {
        self.share_type = Some(share_type.into());
        self.normalize()
    }

    pub fn with_allows_access_requests(mut self, allows_access_requests: bool) -> Result<Self, CloudKitError> {
        self.allows_access_requests = Some(allows_access_requests);
        self.normalize()
    }

    pub fn add_participant(mut self, participant: CKShareParticipant) -> Result<Self, CloudKitError> {
        self.participants.push(participant);
        self.normalize()
    }

    pub fn remove_participant(mut self, participant_id: &str) -> Result<Self, CloudKitError> {
        self.participants.retain(|participant| participant.participant_id() != participant_id);
        self.normalize()
    }

    fn normalize(self) -> Result<Self, CloudKitError> {
        let share_json = json_cstring(&self.to_payload(), "share")?;
        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_share_normalize(share_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::<CKSharePayload>(out_json, "share")? };
        Ok(Self::from_payload(payload))
    }

    pub(crate) fn from_payload(payload: CKSharePayload) -> Self {
        Self {
            share_record: CKRecord::from_payload(payload.share_record),
            root_record: payload.root_record.map(CKRecord::from_payload),
            zone_id: CKRecordZoneID::from_payload(payload.zone_id),
            public_permission: CKShareParticipantPermission::from_raw(payload.public_permission),
            url: payload.url,
            participants: payload
                .participants
                .into_iter()
                .map(CKShareParticipant::from_payload)
                .collect(),
            owner: payload.owner.map(CKShareParticipant::from_payload),
            current_user_participant: payload
                .current_user_participant
                .map(CKShareParticipant::from_payload),
            title: payload.title,
            thumbnail_image_data: payload.thumbnail_image_data,
            share_type: payload.share_type,
            allows_access_requests: payload.allows_access_requests,
            is_zone_wide: payload.is_zone_wide,
        }
    }

    pub(crate) fn to_payload(&self) -> CKSharePayload {
        CKSharePayload {
            share_record: self.share_record.to_payload(),
            root_record: self.root_record.as_ref().map(CKRecord::to_payload),
            zone_id: self.zone_id.to_payload(),
            public_permission: self.public_permission.to_raw(),
            url: self.url.clone(),
            participants: self.participants.iter().map(CKShareParticipant::to_payload).collect(),
            owner: self.owner.as_ref().map(CKShareParticipant::to_payload),
            current_user_participant: self
                .current_user_participant
                .as_ref()
                .map(CKShareParticipant::to_payload),
            title: self.title.clone(),
            thumbnail_image_data: self.thumbnail_image_data.clone(),
            share_type: self.share_type.clone(),
            allows_access_requests: self.allows_access_requests,
            is_zone_wide: self.is_zone_wide,
        }
    }
}