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