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