Skip to main content

bear_cli/cloudkit/
models.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value as JsonValue;
5
6#[derive(Debug, Serialize, Deserialize, Clone)]
7#[serde(rename_all = "camelCase")]
8pub struct ZoneId {
9    pub zone_name: String,
10}
11
12impl Default for ZoneId {
13    fn default() -> Self {
14        Self {
15            zone_name: "Notes".into(),
16        }
17    }
18}
19
20/// A CloudKit field value with type tag and optional encryption flag.
21#[derive(Debug, Serialize, Deserialize, Clone)]
22pub struct CkField {
23    #[serde(rename = "type")]
24    pub kind: String,
25    pub value: JsonValue,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub is_encrypted: Option<bool>,
28}
29
30impl CkField {
31    pub fn string(v: impl Into<String>) -> Self {
32        Self {
33            kind: "STRING".into(),
34            value: JsonValue::String(v.into()),
35            is_encrypted: None,
36        }
37    }
38
39    pub fn string_encrypted(v: impl Into<String>) -> Self {
40        Self {
41            kind: "STRING".into(),
42            value: JsonValue::String(v.into()),
43            is_encrypted: Some(true),
44        }
45    }
46
47    pub fn string_null() -> Self {
48        Self {
49            kind: "STRING".into(),
50            value: JsonValue::Null,
51            is_encrypted: None,
52        }
53    }
54
55    pub fn string_list(v: Vec<String>) -> Self {
56        Self {
57            kind: "STRING_LIST".into(),
58            value: serde_json::to_value(v).unwrap(),
59            is_encrypted: None,
60        }
61    }
62
63    pub fn string_list_null() -> Self {
64        Self {
65            kind: "STRING_LIST".into(),
66            value: JsonValue::Null,
67            is_encrypted: None,
68        }
69    }
70
71    pub fn int64(v: i64) -> Self {
72        Self {
73            kind: "INT64".into(),
74            value: JsonValue::Number(v.into()),
75            is_encrypted: None,
76        }
77    }
78
79    pub fn timestamp(ms: i64) -> Self {
80        Self {
81            kind: "TIMESTAMP".into(),
82            value: JsonValue::Number(ms.into()),
83            is_encrypted: None,
84        }
85    }
86
87    pub fn timestamp_null() -> Self {
88        Self {
89            kind: "TIMESTAMP".into(),
90            value: JsonValue::Null,
91            is_encrypted: None,
92        }
93    }
94
95    pub fn bytes(b64: impl Into<String>) -> Self {
96        Self {
97            kind: "BYTES".into(),
98            value: JsonValue::String(b64.into()),
99            is_encrypted: None,
100        }
101    }
102
103    pub fn asset_id(receipt: AssetReceipt) -> Self {
104        Self {
105            kind: "ASSETID".into(),
106            value: serde_json::to_value(receipt).unwrap(),
107            is_encrypted: None,
108        }
109    }
110}
111
112#[derive(Debug, Serialize, Deserialize, Clone)]
113#[serde(rename_all = "camelCase")]
114pub struct AssetToken {
115    pub record_type: String,
116    pub record_name: String,
117    pub field_name: String,
118}
119
120#[derive(Debug, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct AssetUploadRequest {
123    pub zone_id: ZoneId,
124    pub tokens: Vec<AssetToken>,
125}
126
127#[derive(Debug, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct AssetUploadResponse {
130    pub tokens: Vec<AssetUploadToken>,
131}
132
133#[derive(Debug, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct AssetUploadToken {
136    pub record_name: String,
137    pub field_name: String,
138    pub url: String,
139}
140
141#[derive(Debug, Serialize, Deserialize, Clone)]
142#[serde(rename_all = "camelCase")]
143pub struct AssetReceipt {
144    pub file_checksum: String,
145    pub receipt: String,
146    pub size: i64,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub wrapping_key: Option<String>,
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub reference_checksum: Option<String>,
151}
152
153#[derive(Debug, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct AssetUploadResult {
156    pub single_file: AssetReceipt,
157}
158
159pub type Fields = HashMap<String, CkField>;
160
161#[derive(Debug, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct CkRecord {
164    pub record_name: String,
165    pub record_type: String,
166    pub fields: Fields,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub record_change_tag: Option<String>,
169    #[serde(default)]
170    pub deleted: bool,
171    // Present on error responses:
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub server_error_code: Option<String>,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub reason: Option<String>,
176}
177
178#[derive(Debug, Serialize)]
179#[serde(rename_all = "camelCase")]
180pub struct ModifyOperation {
181    pub operation_type: String, // "create" | "update" | "delete"
182    pub record: CkRecord,
183}
184
185#[derive(Debug, Serialize)]
186#[serde(rename_all = "camelCase")]
187pub struct ModifyRequest {
188    pub operations: Vec<ModifyOperation>,
189    pub zone_id: ZoneId,
190}
191
192#[derive(Debug, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct ModifyResponse {
195    pub records: Vec<CkRecord>,
196}
197
198#[derive(Debug, Serialize, Clone)]
199#[serde(rename_all = "camelCase")]
200pub struct CkQuery {
201    pub record_type: String,
202    #[serde(skip_serializing_if = "Vec::is_empty")]
203    pub filter_by: Vec<CkFilter>,
204    #[serde(skip_serializing_if = "Vec::is_empty")]
205    pub sort_by: Vec<CkSort>,
206}
207
208#[derive(Debug, Serialize, Clone)]
209#[serde(rename_all = "camelCase")]
210pub struct CkFilter {
211    pub field_name: String,
212    pub comparator: String,
213    pub field_value: CkFilterValue,
214}
215
216#[derive(Debug, Serialize, Clone)]
217pub struct CkFilterValue {
218    pub value: JsonValue,
219    #[serde(rename = "type")]
220    pub kind: String,
221}
222
223#[derive(Debug, Serialize, Clone)]
224#[serde(rename_all = "camelCase")]
225pub struct CkSort {
226    pub field_name: String,
227    pub ascending: bool,
228}
229
230#[derive(Debug, Serialize)]
231#[serde(rename_all = "camelCase")]
232pub struct QueryRequest {
233    pub zone_id: ZoneId,
234    pub query: CkQuery,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub results_limit: Option<usize>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub desired_keys: Option<Vec<String>>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub continuation_marker: Option<String>,
241}
242
243#[derive(Debug, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct QueryResponse {
246    pub records: Vec<CkRecord>,
247    #[serde(default)]
248    pub continuation_marker: Option<String>,
249}
250
251#[derive(Debug, Serialize)]
252#[serde(rename_all = "camelCase")]
253pub struct LookupRequest {
254    pub records: Vec<LookupRecord>,
255    pub zone_id: ZoneId,
256}
257
258#[derive(Debug, Serialize)]
259#[serde(rename_all = "camelCase")]
260pub struct LookupRecord {
261    pub record_name: String,
262}
263
264#[derive(Debug, Deserialize)]
265#[serde(rename_all = "camelCase")]
266pub struct LookupResponse {
267    pub records: Vec<CkRecord>,
268}
269
270impl CkRecord {
271    pub fn str_field(&self, name: &str) -> Option<&str> {
272        self.fields.get(name)?.value.as_str()
273    }
274
275    pub fn i64_field(&self, name: &str) -> Option<i64> {
276        self.fields.get(name)?.value.as_i64()
277    }
278
279    pub fn bool_field(&self, name: &str) -> Option<bool> {
280        self.i64_field(name).map(|v| v != 0)
281    }
282
283    pub fn string_list_field(&self, name: &str) -> Vec<String> {
284        self.fields
285            .get(name)
286            .and_then(|f| f.value.as_array())
287            .map(|arr| {
288                arr.iter()
289                    .filter_map(|v| v.as_str().map(str::to_string))
290                    .collect()
291            })
292            .unwrap_or_default()
293    }
294}