Skip to main content

cloudkit/
record.rs

1use std::collections::BTreeMap;
2use std::ffi::c_char;
3use std::ops::{BitOr, BitOrAssign};
4use std::path::{Path, PathBuf};
5
6use crate::error::CloudKitError;
7use crate::ffi;
8use crate::private::{
9    cstring_from_str, error_from_status, parse_json_ptr, CKAssetPayload, CKRecordIDPayload,
10    CKRecordPayload, CKRecordValuePayload, CKRecordZoneIDPayload, CKRecordZonePayload,
11    CKReferencePayload, RecordValueKind,
12};
13use crate::reference_utility::{CKReference, CKReferenceAction};
14
15const DEFAULT_ZONE_NAME: &str = "_defaultZone";
16const DEFAULT_OWNER_NAME: &str = "__defaultOwner__";
17
18/// Wraps `CKRecordZoneCapabilities`.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
20pub struct CKRecordZoneCapabilities(u64);
21
22impl CKRecordZoneCapabilities {
23    /// Mirrors the `fetchChanges` option on `CKRecordZoneCapabilities`.
24    pub const FETCH_CHANGES: Self = Self(1 << 0);
25    /// Mirrors the `atomic` option on `CKRecordZoneCapabilities`.
26    pub const ATOMIC: Self = Self(1 << 1);
27    /// Mirrors the `sharing` option on `CKRecordZoneCapabilities`.
28    pub const SHARING: Self = Self(1 << 2);
29    /// Mirrors the `zoneWideSharing` option on `CKRecordZoneCapabilities`.
30    pub const ZONE_WIDE_SHARING: Self = Self(1 << 3);
31
32    /// Creates a wrapper mirroring `CKRecordZoneCapabilities`.
33    pub const fn new(bits: u64) -> Self {
34        Self(bits)
35    }
36
37    /// Mirrors `CKRecordZoneCapabilities.rawValue`.
38    pub const fn bits(self) -> u64 {
39        self.0
40    }
41
42    /// Mirrors `CKRecordZoneCapabilities.contains(_:)`.
43    pub const fn contains(self, other: Self) -> bool {
44        (self.0 & other.0) == other.0
45    }
46}
47
48impl BitOr for CKRecordZoneCapabilities {
49    type Output = Self;
50
51    fn bitor(self, rhs: Self) -> Self::Output {
52        Self(self.0 | rhs.0)
53    }
54}
55
56impl BitOrAssign for CKRecordZoneCapabilities {
57    fn bitor_assign(&mut self, rhs: Self) {
58        self.0 |= rhs.0;
59    }
60}
61
62/// Mirrors `CKRecordZoneEncryptionScope`.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64#[non_exhaustive]
65pub enum CKRecordZoneEncryptionScope {
66    /// Mirrors `CKRecordZoneEncryptionScope.perRecord`.
67    PerRecord,
68    /// Mirrors `CKRecordZoneEncryptionScope.perZone`.
69    PerZone,
70    /// Mirrors `CKRecordZoneEncryptionScope.unknown`.
71    Unknown(i32),
72}
73
74impl CKRecordZoneEncryptionScope {
75    pub(crate) const fn from_raw(raw: i32) -> Self {
76        match raw {
77            0 => Self::PerRecord,
78            1 => Self::PerZone,
79            other => Self::Unknown(other),
80        }
81    }
82
83    pub(crate) const fn to_raw(self) -> i32 {
84        match self {
85            Self::PerRecord => 0,
86            Self::PerZone => 1,
87            Self::Unknown(raw) => raw,
88        }
89    }
90}
91
92/// Wraps `CKRecordZoneID`.
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
94pub struct CKRecordZoneID {
95    zone_name: String,
96    owner_name: String,
97}
98
99impl CKRecordZoneID {
100    /// Creates a wrapper mirroring `CKRecordZoneID`.
101    pub fn new(zone_name: impl Into<String>, owner_name: impl Into<String>) -> Self {
102        Self {
103            zone_name: zone_name.into(),
104            owner_name: owner_name.into(),
105        }
106    }
107
108    /// Mirrors `CKRecordZoneID.defaultZone`.
109    pub fn default_zone() -> Self {
110        Self::new(DEFAULT_ZONE_NAME, DEFAULT_OWNER_NAME)
111    }
112
113    /// Mirrors `CKRecordZoneID.zoneName`.
114    pub fn zone_name(&self) -> &str {
115        &self.zone_name
116    }
117
118    /// Mirrors `CKRecordZoneID.ownerName`.
119    pub fn owner_name(&self) -> &str {
120        &self.owner_name
121    }
122
123    pub(crate) fn from_payload(payload: CKRecordZoneIDPayload) -> Self {
124        Self::new(payload.zone_name, payload.owner_name)
125    }
126
127    pub(crate) fn to_payload(&self) -> CKRecordZoneIDPayload {
128        CKRecordZoneIDPayload {
129            zone_name: self.zone_name.clone(),
130            owner_name: self.owner_name.clone(),
131        }
132    }
133}
134
135/// Wraps `CKRecordID`.
136#[derive(Debug, Clone, PartialEq, Eq, Hash)]
137pub struct CKRecordID {
138    record_name: String,
139    zone_id: CKRecordZoneID,
140}
141
142impl CKRecordID {
143    /// Creates a wrapper mirroring `CKRecordID`.
144    pub fn new(record_name: impl Into<String>) -> Self {
145        Self::with_zone(record_name, CKRecordZoneID::default_zone())
146    }
147
148    /// Sets the value mirroring `CKRecordID.zone`.
149    pub fn with_zone(record_name: impl Into<String>, zone_id: CKRecordZoneID) -> Self {
150        Self {
151            record_name: record_name.into(),
152            zone_id,
153        }
154    }
155
156    /// Mirrors `CKRecordID.recordName`.
157    pub fn record_name(&self) -> &str {
158        &self.record_name
159    }
160
161    /// Mirrors `CKRecordID.zoneID`.
162    pub fn zone_id(&self) -> &CKRecordZoneID {
163        &self.zone_id
164    }
165
166    pub(crate) fn from_payload(payload: CKRecordIDPayload) -> Self {
167        Self::with_zone(
168            payload.record_name,
169            CKRecordZoneID::from_payload(payload.zone_id),
170        )
171    }
172
173    pub(crate) fn to_payload(&self) -> CKRecordIDPayload {
174        CKRecordIDPayload {
175            record_name: self.record_name.clone(),
176            zone_id: self.zone_id.to_payload(),
177        }
178    }
179}
180
181/// Wraps `CKAsset`.
182#[derive(Debug, Clone, PartialEq, Eq, Hash)]
183pub struct CKAsset {
184    file_url: PathBuf,
185}
186
187impl CKAsset {
188    /// Creates a wrapper mirroring `CKAsset`.
189    pub fn new(path: impl Into<PathBuf>) -> Self {
190        Self {
191            file_url: path.into(),
192        }
193    }
194
195    /// Mirrors `CKAsset.fileURL`.
196    pub fn file_url(&self) -> &Path {
197        &self.file_url
198    }
199
200    pub(crate) fn from_payload(payload: CKAssetPayload) -> Self {
201        Self::new(payload.file_url)
202    }
203
204    pub(crate) fn to_payload(&self) -> CKAssetPayload {
205        CKAssetPayload {
206            file_url: self.file_url.to_string_lossy().into_owned(),
207        }
208    }
209}
210
211/// Mirrors the typed field values stored by `CKRecord`.
212#[derive(Debug, Clone, PartialEq)]
213#[non_exhaustive]
214pub enum RecordValue {
215    /// Mirrors `CKRecord.string`.
216    String(String),
217    /// Mirrors `CKRecord.int`.
218    Int(i64),
219    /// Mirrors `CKRecord.double`.
220    Double(f64),
221    /// Mirrors `CKRecord.bool`.
222    Bool(bool),
223    /// Mirrors `CKRecord.bytes`.
224    Bytes(Vec<u8>),
225    /// Mirrors `CKRecord.date`.
226    Date(String),
227    /// Mirrors `CKRecord.asset`.
228    Asset(CKAsset),
229    /// Mirrors `CKRecord.reference`.
230    Reference(CKReference),
231    /// Mirrors `CKRecord.array`.
232    Array(Vec<RecordValue>),
233}
234
235impl From<String> for RecordValue {
236    fn from(value: String) -> Self {
237        Self::String(value)
238    }
239}
240
241impl From<&str> for RecordValue {
242    fn from(value: &str) -> Self {
243        Self::String(value.into())
244    }
245}
246
247impl From<bool> for RecordValue {
248    fn from(value: bool) -> Self {
249        Self::Bool(value)
250    }
251}
252
253impl From<i64> for RecordValue {
254    fn from(value: i64) -> Self {
255        Self::Int(value)
256    }
257}
258
259impl From<i32> for RecordValue {
260    fn from(value: i32) -> Self {
261        Self::Int(i64::from(value))
262    }
263}
264
265impl From<f64> for RecordValue {
266    fn from(value: f64) -> Self {
267        Self::Double(value)
268    }
269}
270
271impl From<Vec<u8>> for RecordValue {
272    fn from(value: Vec<u8>) -> Self {
273        Self::Bytes(value)
274    }
275}
276
277impl From<CKAsset> for RecordValue {
278    fn from(value: CKAsset) -> Self {
279        Self::Asset(value)
280    }
281}
282
283impl From<CKReference> for RecordValue {
284    fn from(value: CKReference) -> Self {
285        Self::Reference(value)
286    }
287}
288
289impl From<Vec<RecordValue>> for RecordValue {
290    fn from(value: Vec<RecordValue>) -> Self {
291        Self::Array(value)
292    }
293}
294
295impl RecordValue {
296    pub(crate) fn from_payload(payload: CKRecordValuePayload) -> Self {
297        match payload.kind {
298            RecordValueKind::String => Self::String(payload.string_value.unwrap_or_default()),
299            RecordValueKind::Int => Self::Int(payload.int_value.unwrap_or_default()),
300            RecordValueKind::Double => Self::Double(payload.double_value.unwrap_or_default()),
301            RecordValueKind::Bool => Self::Bool(payload.bool_value.unwrap_or_default()),
302            RecordValueKind::Bytes => Self::Bytes(payload.bytes_value.unwrap_or_default()),
303            RecordValueKind::Date => Self::Date(payload.date_value.unwrap_or_default()),
304            RecordValueKind::Asset => Self::Asset(CKAsset::from_payload(
305                payload.asset_value.unwrap_or(CKAssetPayload {
306                    file_url: String::new(),
307                }),
308            )),
309            RecordValueKind::Reference => Self::Reference(CKReference::from_payload(
310                payload
311                    .reference_value
312                    .unwrap_or_else(|| CKReferencePayload {
313                        record_id: CKRecordID::new(String::new()).to_payload(),
314                        action: CKReferenceAction::None.to_raw(),
315                    }),
316            )),
317            RecordValueKind::Array => Self::Array(
318                payload
319                    .array_value
320                    .unwrap_or_default()
321                    .into_iter()
322                    .map(Self::from_payload)
323                    .collect(),
324            ),
325        }
326    }
327
328    pub(crate) fn to_payload(&self) -> CKRecordValuePayload {
329        match self {
330            Self::String(value) => CKRecordValuePayload {
331                kind: RecordValueKind::String,
332                string_value: Some(value.clone()),
333                int_value: None,
334                double_value: None,
335                bool_value: None,
336                bytes_value: None,
337                date_value: None,
338                asset_value: None,
339                reference_value: None,
340                array_value: None,
341            },
342            Self::Int(value) => CKRecordValuePayload {
343                kind: RecordValueKind::Int,
344                string_value: None,
345                int_value: Some(*value),
346                double_value: None,
347                bool_value: None,
348                bytes_value: None,
349                date_value: None,
350                asset_value: None,
351                reference_value: None,
352                array_value: None,
353            },
354            Self::Double(value) => CKRecordValuePayload {
355                kind: RecordValueKind::Double,
356                string_value: None,
357                int_value: None,
358                double_value: Some(*value),
359                bool_value: None,
360                bytes_value: None,
361                date_value: None,
362                asset_value: None,
363                reference_value: None,
364                array_value: None,
365            },
366            Self::Bool(value) => CKRecordValuePayload {
367                kind: RecordValueKind::Bool,
368                string_value: None,
369                int_value: None,
370                double_value: None,
371                bool_value: Some(*value),
372                bytes_value: None,
373                date_value: None,
374                asset_value: None,
375                reference_value: None,
376                array_value: None,
377            },
378            Self::Bytes(value) => CKRecordValuePayload {
379                kind: RecordValueKind::Bytes,
380                string_value: None,
381                int_value: None,
382                double_value: None,
383                bool_value: None,
384                bytes_value: Some(value.clone()),
385                date_value: None,
386                asset_value: None,
387                reference_value: None,
388                array_value: None,
389            },
390            Self::Date(value) => CKRecordValuePayload {
391                kind: RecordValueKind::Date,
392                string_value: None,
393                int_value: None,
394                double_value: None,
395                bool_value: None,
396                bytes_value: None,
397                date_value: Some(value.clone()),
398                asset_value: None,
399                reference_value: None,
400                array_value: None,
401            },
402            Self::Asset(asset) => CKRecordValuePayload {
403                kind: RecordValueKind::Asset,
404                string_value: None,
405                int_value: None,
406                double_value: None,
407                bool_value: None,
408                bytes_value: None,
409                date_value: None,
410                asset_value: Some(asset.to_payload()),
411                reference_value: None,
412                array_value: None,
413            },
414            Self::Reference(reference) => CKRecordValuePayload {
415                kind: RecordValueKind::Reference,
416                string_value: None,
417                int_value: None,
418                double_value: None,
419                bool_value: None,
420                bytes_value: None,
421                date_value: None,
422                asset_value: None,
423                reference_value: Some(reference.to_payload()),
424                array_value: None,
425            },
426            Self::Array(values) => CKRecordValuePayload {
427                kind: RecordValueKind::Array,
428                string_value: None,
429                int_value: None,
430                double_value: None,
431                bool_value: None,
432                bytes_value: None,
433                date_value: None,
434                asset_value: None,
435                reference_value: None,
436                array_value: Some(values.iter().map(Self::to_payload).collect()),
437            },
438        }
439    }
440}
441
442/// Wraps `CKRecordZone`.
443#[derive(Debug, Clone, PartialEq, Eq, Hash)]
444pub struct CKRecordZone {
445    zone_id: CKRecordZoneID,
446    capabilities: CKRecordZoneCapabilities,
447    share: Option<CKReference>,
448    encryption_scope: Option<CKRecordZoneEncryptionScope>,
449}
450
451impl CKRecordZone {
452    /// Creates a wrapper mirroring `CKRecordZone`.
453    pub fn new(zone_name: impl Into<String>) -> Self {
454        Self {
455            zone_id: CKRecordZoneID::new(zone_name, DEFAULT_OWNER_NAME),
456            capabilities: CKRecordZoneCapabilities::default(),
457            share: None,
458            encryption_scope: Some(CKRecordZoneEncryptionScope::PerRecord),
459        }
460    }
461
462    /// Sets the value mirroring `CKRecordZone.zoneID`.
463    pub fn with_zone_id(zone_id: CKRecordZoneID) -> Self {
464        Self {
465            zone_id,
466            capabilities: CKRecordZoneCapabilities::default(),
467            share: None,
468            encryption_scope: Some(CKRecordZoneEncryptionScope::PerRecord),
469        }
470    }
471
472    /// Mirrors `CKRecordZone.defaultZone`.
473    pub fn default_zone() -> Self {
474        Self::with_zone_id(CKRecordZoneID::default_zone())
475    }
476
477    /// Mirrors `CKRecordZone.zoneID`.
478    pub fn zone_id(&self) -> &CKRecordZoneID {
479        &self.zone_id
480    }
481
482    /// Mirrors `CKRecordZone.capabilities`.
483    pub const fn capabilities(&self) -> CKRecordZoneCapabilities {
484        self.capabilities
485    }
486
487    /// Mirrors `CKRecordZone.share`.
488    pub const fn share(&self) -> Option<&CKReference> {
489        self.share.as_ref()
490    }
491
492    /// Mirrors `CKRecordZone.encryptionScope`.
493    pub const fn encryption_scope(&self) -> Option<CKRecordZoneEncryptionScope> {
494        self.encryption_scope
495    }
496
497    pub(crate) fn from_payload(payload: CKRecordZonePayload) -> Self {
498        Self {
499            zone_id: CKRecordZoneID::from_payload(payload.zone_id),
500            capabilities: CKRecordZoneCapabilities::new(payload.capabilities),
501            share: payload.share.map(CKReference::from_payload),
502            encryption_scope: payload
503                .encryption_scope
504                .map(CKRecordZoneEncryptionScope::from_raw),
505        }
506    }
507
508    pub(crate) fn to_payload(&self) -> CKRecordZonePayload {
509        CKRecordZonePayload {
510            zone_id: self.zone_id.to_payload(),
511            capabilities: self.capabilities.bits(),
512            share: self.share.as_ref().map(CKReference::to_payload),
513            encryption_scope: self
514                .encryption_scope
515                .map(CKRecordZoneEncryptionScope::to_raw),
516        }
517    }
518}
519
520/// Wraps `CKRecord`.
521#[derive(Debug, Clone, PartialEq)]
522pub struct CKRecord {
523    record_type: String,
524    record_id: CKRecordID,
525    fields: BTreeMap<String, RecordValue>,
526    encoded_system_fields: Vec<u8>,
527    record_change_tag: Option<String>,
528    creator_user_record_id: Option<CKRecordID>,
529    creation_date: Option<String>,
530    last_modified_user_record_id: Option<CKRecordID>,
531    modification_date: Option<String>,
532    parent: Option<CKReference>,
533    share: Option<CKReference>,
534    changed_keys: Vec<String>,
535    all_tokens: Vec<String>,
536}
537
538impl CKRecord {
539    /// Creates a wrapper mirroring `CKRecord`.
540    pub fn new(record_type: &str) -> Result<Self, CloudKitError> {
541        let record_type = cstring_from_str(record_type, "record type")?;
542        let mut out_json: *mut c_char = core::ptr::null_mut();
543        let mut out_error: *mut c_char = core::ptr::null_mut();
544        let status =
545            unsafe { ffi::ck_record_create(record_type.as_ptr(), &mut out_json, &mut out_error) };
546        if status != ffi::status::OK {
547            return Err(unsafe { error_from_status(status, out_error) });
548        }
549        let payload: CKRecordPayload = unsafe { parse_json_ptr(out_json, "record")? };
550        Ok(Self::from_payload(payload))
551    }
552
553    /// Sets the value mirroring `CKRecord.recordID`.
554    pub fn with_record_id(record_type: impl Into<String>, record_id: CKRecordID) -> Self {
555        Self {
556            record_type: record_type.into(),
557            record_id,
558            fields: BTreeMap::new(),
559            encoded_system_fields: Vec::new(),
560            record_change_tag: None,
561            creator_user_record_id: None,
562            creation_date: None,
563            last_modified_user_record_id: None,
564            modification_date: None,
565            parent: None,
566            share: None,
567            changed_keys: Vec::new(),
568            all_tokens: Vec::new(),
569        }
570    }
571
572    /// Sets the value mirroring `CKRecord.zone`.
573    pub fn with_zone(record_type: impl Into<String>, zone_id: CKRecordZoneID) -> Self {
574        Self::with_record_id(record_type, CKRecordID::with_zone(String::new(), zone_id))
575    }
576
577    /// Mirrors `CKRecord.recordType`.
578    pub fn record_type(&self) -> &str {
579        &self.record_type
580    }
581
582    /// Mirrors `CKRecord.recordID`.
583    pub fn record_id(&self) -> &CKRecordID {
584        &self.record_id
585    }
586
587    /// Mirrors `CKRecord.recordChangeTag`.
588    pub fn record_change_tag(&self) -> Option<&str> {
589        self.record_change_tag.as_deref()
590    }
591
592    /// Mirrors `CKRecord.creatorUserRecordID`.
593    pub const fn creator_user_record_id(&self) -> Option<&CKRecordID> {
594        self.creator_user_record_id.as_ref()
595    }
596
597    /// Mirrors `CKRecord.creationDate`.
598    pub fn creation_date(&self) -> Option<&str> {
599        self.creation_date.as_deref()
600    }
601
602    /// Mirrors `CKRecord.lastModifiedUserRecordID`.
603    pub const fn last_modified_user_record_id(&self) -> Option<&CKRecordID> {
604        self.last_modified_user_record_id.as_ref()
605    }
606
607    /// Mirrors `CKRecord.modificationDate`.
608    pub fn modification_date(&self) -> Option<&str> {
609        self.modification_date.as_deref()
610    }
611
612    /// Mirrors `CKRecord.parent`.
613    pub const fn parent(&self) -> Option<&CKReference> {
614        self.parent.as_ref()
615    }
616
617    /// Mirrors `CKRecord.share`.
618    pub const fn share(&self) -> Option<&CKReference> {
619        self.share.as_ref()
620    }
621
622
623    /// Mirrors `CKRecord.changedKeys`.
624    pub fn changed_keys(&self) -> &[String] {
625        &self.changed_keys
626    }
627
628    /// Mirrors `CKRecord.allTokens`.
629    pub fn all_tokens(&self) -> &[String] {
630        &self.all_tokens
631    }
632
633    /// Mirrors `CKRecord.object`.
634    pub fn object(&self, key: &str) -> Option<&RecordValue> {
635        self.fields.get(key)
636    }
637
638    /// Mirrors `CKRecord.setObject`.
639    pub fn set_object<V>(&mut self, key: &str, value: V)
640    where
641        V: Into<RecordValue>,
642    {
643        self.fields.insert(key.into(), value.into());
644        self.mark_changed_key(key);
645    }
646
647    /// Mirrors `CKRecord.removeObject`.
648    pub fn remove_object(&mut self, key: &str) -> Option<RecordValue> {
649        let removed = self.fields.remove(key);
650        if removed.is_some() {
651            self.mark_changed_key(key);
652        }
653        removed
654    }
655
656    /// Mirrors `CKRecord.allKeys`.
657    pub fn all_keys(&self) -> Vec<String> {
658        self.fields.keys().cloned().collect()
659    }
660
661    /// Mirrors `CKRecord.encodedSystemFields`.
662    pub fn encoded_system_fields(&self) -> &[u8] {
663        &self.encoded_system_fields
664    }
665
666    /// Mirrors `CKRecord.setParentReference`.
667    pub fn set_parent_reference(&mut self, reference: CKReference) {
668        self.parent = Some(reference);
669    }
670
671    /// Mirrors `CKRecord.setParentReferenceFromRecord`.
672    pub fn set_parent_reference_from_record(&mut self, parent_record: &CKRecord) {
673        self.set_parent_reference(CKReference::new(
674            parent_record.record_id().clone(),
675            CKReferenceAction::None,
676        ));
677    }
678
679    /// Mirrors `CKRecord.setParentReferenceFromRecordID`.
680    pub fn set_parent_reference_from_record_id(&mut self, parent_record_id: CKRecordID) {
681        self.set_parent_reference(CKReference::new(parent_record_id, CKReferenceAction::None));
682    }
683
684    /// Mirrors `CKRecord.clearParentReference`.
685    pub fn clear_parent_reference(&mut self) {
686        self.parent = None;
687    }
688
689    fn mark_changed_key(&mut self, key: &str) {
690        if !self.changed_keys.iter().any(|changed| changed == key) {
691            self.changed_keys.push(key.into());
692        }
693    }
694
695    pub(crate) fn from_payload(payload: CKRecordPayload) -> Self {
696        Self {
697            record_type: payload.record_type,
698            record_id: CKRecordID::from_payload(payload.record_id),
699            fields: payload
700                .fields
701                .into_iter()
702                .map(|(key, value)| (key, RecordValue::from_payload(value)))
703                .collect(),
704            encoded_system_fields: payload.encoded_system_fields,
705            record_change_tag: payload.record_change_tag,
706            creator_user_record_id: payload.creator_user_record_id.map(CKRecordID::from_payload),
707            creation_date: payload.creation_date,
708            last_modified_user_record_id: payload
709                .last_modified_user_record_id
710                .map(CKRecordID::from_payload),
711            modification_date: payload.modification_date,
712            parent: payload.parent.map(CKReference::from_payload),
713            share: payload.share.map(CKReference::from_payload),
714            changed_keys: payload.changed_keys,
715            all_tokens: payload.all_tokens,
716        }
717    }
718
719    pub(crate) fn to_payload(&self) -> CKRecordPayload {
720        CKRecordPayload {
721            record_type: self.record_type.clone(),
722            record_id: self.record_id.to_payload(),
723            fields: self
724                .fields
725                .iter()
726                .map(|(key, value)| (key.clone(), value.to_payload()))
727                .collect(),
728            encoded_system_fields: self.encoded_system_fields.clone(),
729            record_change_tag: self.record_change_tag.clone(),
730            creator_user_record_id: self
731                .creator_user_record_id
732                .as_ref()
733                .map(CKRecordID::to_payload),
734            creation_date: self.creation_date.clone(),
735            last_modified_user_record_id: self
736                .last_modified_user_record_id
737                .as_ref()
738                .map(CKRecordID::to_payload),
739            modification_date: self.modification_date.clone(),
740            parent: self.parent.as_ref().map(CKReference::to_payload),
741            share: self.share.as_ref().map(CKReference::to_payload),
742            changed_keys: self.changed_keys.clone(),
743            all_tokens: self.all_tokens.clone(),
744        }
745    }
746}
747
748/// Accepts values that map to `CKRecord` fields.
749pub trait CKRecordKeyValueSetting {
750    /// Mirrors `CKRecord.objectForKey`.
751    fn object_for_key(&self, key: &str) -> Option<&RecordValue>;
752    /// Mirrors `CKRecord.setObjectForKey`.
753    fn set_object_for_key<V>(&mut self, key: &str, value: V)
754    where
755        V: Into<RecordValue>;
756    /// Mirrors `CKRecord.objectForKeyedSubscript`.
757    fn object_for_keyed_subscript(&self, key: &str) -> Option<&RecordValue>;
758    /// Mirrors `CKRecord.setObjectForKeyedSubscript`.
759    fn set_object_for_keyed_subscript<V>(&mut self, key: &str, value: V)
760    where
761        V: Into<RecordValue>;
762    /// Mirrors `CKRecord.allKeys`.
763    fn all_keys(&self) -> Vec<String>;
764    /// Mirrors `CKRecord.changedKeys`.
765    fn changed_keys(&self) -> &[String];
766}
767
768impl CKRecordKeyValueSetting for CKRecord {
769    fn object_for_key(&self, key: &str) -> Option<&RecordValue> {
770        self.object(key)
771    }
772
773    fn set_object_for_key<V>(&mut self, key: &str, value: V)
774    where
775        V: Into<RecordValue>,
776    {
777        self.set_object(key, value);
778    }
779
780    fn object_for_keyed_subscript(&self, key: &str) -> Option<&RecordValue> {
781        self.object(key)
782    }
783
784    fn set_object_for_keyed_subscript<V>(&mut self, key: &str, value: V)
785    where
786        V: Into<RecordValue>,
787    {
788        self.set_object(key, value);
789    }
790
791    fn all_keys(&self) -> Vec<String> {
792        self.all_keys()
793    }
794
795    fn changed_keys(&self) -> &[String] {
796        self.changed_keys()
797    }
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803
804    #[test]
805    fn zone_capabilities_support_bit_operations() {
806        let mut capabilities =
807            CKRecordZoneCapabilities::FETCH_CHANGES | CKRecordZoneCapabilities::SHARING;
808
809        assert_eq!(capabilities.bits(), 5);
810        assert!(capabilities.contains(CKRecordZoneCapabilities::FETCH_CHANGES));
811        assert!(capabilities.contains(CKRecordZoneCapabilities::SHARING));
812        assert!(!capabilities.contains(CKRecordZoneCapabilities::ATOMIC));
813
814        capabilities |= CKRecordZoneCapabilities::ATOMIC;
815
816        assert!(capabilities.contains(CKRecordZoneCapabilities::ATOMIC));
817        assert_eq!(capabilities.bits(), 7);
818    }
819
820    #[test]
821    fn encryption_scope_round_trips_known_and_unknown_values() {
822        assert_eq!(
823            CKRecordZoneEncryptionScope::from_raw(0),
824            CKRecordZoneEncryptionScope::PerRecord
825        );
826        assert_eq!(
827            CKRecordZoneEncryptionScope::from_raw(1),
828            CKRecordZoneEncryptionScope::PerZone
829        );
830        assert_eq!(
831            CKRecordZoneEncryptionScope::from_raw(42),
832            CKRecordZoneEncryptionScope::Unknown(42)
833        );
834        assert_eq!(CKRecordZoneEncryptionScope::PerZone.to_raw(), 1);
835        assert_eq!(CKRecordZoneEncryptionScope::Unknown(42).to_raw(), 42);
836    }
837
838    #[test]
839    fn zone_ids_round_trip_and_default_zone_matches_constants() {
840        let default_zone = CKRecordZoneID::default_zone();
841        let zone = CKRecordZoneID::new("CustomZone", "owner-1");
842
843        assert_eq!(default_zone.zone_name(), "_defaultZone");
844        assert_eq!(default_zone.owner_name(), "__defaultOwner__");
845        assert_eq!(CKRecordZoneID::from_payload(zone.to_payload()), zone);
846    }
847
848    #[test]
849    fn record_ids_round_trip_with_custom_zone() {
850        let zone = CKRecordZoneID::new("Photos", "owner-2");
851        let record_id = CKRecordID::with_zone("record-1", zone.clone());
852
853        assert_eq!(record_id.record_name(), "record-1");
854        assert_eq!(record_id.zone_id(), &zone);
855        assert_eq!(CKRecordID::from_payload(record_id.to_payload()), record_id);
856    }
857
858    #[test]
859    fn assets_round_trip_payloads() {
860        let asset = CKAsset::new("cover.heic");
861
862        assert_eq!(CKAsset::from_payload(asset.to_payload()), asset);
863    }
864
865    #[test]
866    fn record_values_round_trip_nested_payloads() {
867        let record_value = RecordValue::Array(vec![
868            RecordValue::from("headline"),
869            RecordValue::from(42_i32),
870            RecordValue::from(true),
871            RecordValue::Reference(CKReference::delete_self(CKRecordID::new("child"))),
872            RecordValue::from(CKAsset::new("cover.heic")),
873        ]);
874
875        assert_eq!(RecordValue::from_payload(record_value.to_payload()), record_value);
876    }
877
878    #[test]
879    fn records_with_zone_start_with_expected_defaults() {
880        let zone = CKRecordZoneID::new("Drafts", "owner-3");
881        let record = CKRecord::with_zone("Photo", zone.clone());
882
883        assert_eq!(record.record_type(), "Photo");
884        assert_eq!(record.record_id().record_name(), "");
885        assert_eq!(record.record_id().zone_id(), &zone);
886        assert!(record.record_change_tag().is_none());
887        assert!(record.creator_user_record_id().is_none());
888        assert!(record.creation_date().is_none());
889        assert!(record.last_modified_user_record_id().is_none());
890        assert!(record.modification_date().is_none());
891        assert!(record.parent().is_none());
892        assert!(record.share().is_none());
893    }
894}