1use std::fmt;
6
7use crate::storage::bookmarks::BookmarkRootGuid;
8use crate::types::UnknownFields;
9use serde::{
10 de::{Deserialize, Deserializer, Visitor},
11 ser::{Serialize, Serializer},
12};
13use serde_derive::*;
14use sync_guid::Guid as SyncGuid;
15
16#[derive(Clone, Debug, Hash, PartialEq, Eq)]
29pub struct BookmarkRecordId(SyncGuid);
30
31impl BookmarkRecordId {
32 pub fn from_payload_id(payload_id: SyncGuid) -> BookmarkRecordId {
34 BookmarkRecordId(match payload_id.as_str() {
35 "places" => BookmarkRootGuid::Root.as_guid(),
36 "menu" => BookmarkRootGuid::Menu.as_guid(),
37 "toolbar" => BookmarkRootGuid::Toolbar.as_guid(),
38 "unfiled" => BookmarkRootGuid::Unfiled.as_guid(),
39 "mobile" => BookmarkRootGuid::Mobile.as_guid(),
40 _ => payload_id,
41 })
42 }
43
44 #[inline]
47 pub fn as_payload_id(&self) -> &str {
48 self.root_payload_id().unwrap_or_else(|| self.0.as_ref())
49 }
50
51 #[inline]
55 pub fn into_payload_id(self) -> SyncGuid {
56 self.root_payload_id().map(Into::into).unwrap_or(self.0)
57 }
58
59 #[inline]
61 pub fn as_guid(&self) -> &SyncGuid {
62 &self.0
63 }
64
65 fn root_payload_id(&self) -> Option<&str> {
66 Some(match BookmarkRootGuid::from_guid(self.as_guid()) {
67 Some(BookmarkRootGuid::Root) => "places",
68 Some(BookmarkRootGuid::Menu) => "menu",
69 Some(BookmarkRootGuid::Toolbar) => "toolbar",
70 Some(BookmarkRootGuid::Unfiled) => "unfiled",
71 Some(BookmarkRootGuid::Mobile) => "mobile",
72 None => return None,
73 })
74 }
75}
76
77impl From<SyncGuid> for BookmarkRecordId {
79 #[inline]
80 fn from(guid: SyncGuid) -> BookmarkRecordId {
81 BookmarkRecordId(guid)
82 }
83}
84
85impl From<BookmarkRecordId> for SyncGuid {
87 #[inline]
88 fn from(record_id: BookmarkRecordId) -> SyncGuid {
89 record_id.0
90 }
91}
92
93impl Serialize for BookmarkRecordId {
94 #[inline]
95 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
96 serializer.serialize_str(self.as_payload_id())
97 }
98}
99
100impl<'de> Deserialize<'de> for BookmarkRecordId {
101 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
102 struct V;
103
104 impl Visitor<'_> for V {
105 type Value = BookmarkRecordId;
106
107 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 f.write_str("a bookmark record ID")
109 }
110
111 #[inline]
112 fn visit_string<E: serde::de::Error>(
113 self,
114 payload_id: String,
115 ) -> std::result::Result<BookmarkRecordId, E> {
116 Ok(BookmarkRecordId::from_payload_id(payload_id.into()))
119 }
120
121 #[inline]
122 fn visit_str<E: serde::de::Error>(
123 self,
124 payload_id: &str,
125 ) -> std::result::Result<BookmarkRecordId, E> {
126 Ok(BookmarkRecordId::from_payload_id(payload_id.into()))
130 }
131 }
132
133 deserializer.deserialize_string(V)
134 }
135}
136
137#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
138#[serde(rename_all = "camelCase")]
139pub struct BookmarkRecord {
140 #[serde(rename = "id")]
143 pub record_id: BookmarkRecordId,
144
145 #[serde(rename = "parentid")]
146 pub parent_record_id: Option<BookmarkRecordId>,
147
148 #[serde(rename = "parentName", skip_serializing_if = "Option::is_none")]
149 pub parent_title: Option<String>,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
152 #[serde(default, deserialize_with = "de_maybe_stringified_timestamp")]
153 pub date_added: Option<i64>,
154
155 #[serde(default)]
156 pub has_dupe: bool,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub title: Option<String>,
160
161 #[serde(rename = "bmkUri", skip_serializing_if = "Option::is_none")]
162 pub url: Option<String>,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub keyword: Option<String>,
166
167 #[serde(default, skip_serializing_if = "Vec::is_empty")]
168 pub tags: Vec<String>,
169
170 #[serde(flatten)]
171 pub unknown_fields: UnknownFields,
172}
173
174impl From<BookmarkRecord> for BookmarkItemRecord {
175 #[inline]
176 fn from(b: BookmarkRecord) -> BookmarkItemRecord {
177 BookmarkItemRecord::Bookmark(b)
178 }
179}
180
181#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
182#[serde(rename_all = "camelCase")]
183pub struct QueryRecord {
184 #[serde(rename = "id")]
185 pub record_id: BookmarkRecordId,
186
187 #[serde(rename = "parentid")]
188 pub parent_record_id: Option<BookmarkRecordId>,
189
190 #[serde(rename = "parentName", skip_serializing_if = "Option::is_none")]
191 pub parent_title: Option<String>,
192
193 #[serde(skip_serializing_if = "Option::is_none")]
194 #[serde(default, deserialize_with = "de_maybe_stringified_timestamp")]
195 pub date_added: Option<i64>,
196
197 #[serde(default)]
198 pub has_dupe: bool,
199
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub title: Option<String>,
202
203 #[serde(rename = "bmkUri", skip_serializing_if = "Option::is_none")]
204 pub url: Option<String>,
205
206 #[serde(rename = "folderName", skip_serializing_if = "Option::is_none")]
207 pub tag_folder_name: Option<String>,
208
209 #[serde(flatten)]
210 pub unknown_fields: UnknownFields,
211}
212
213impl From<QueryRecord> for BookmarkItemRecord {
214 #[inline]
215 fn from(q: QueryRecord) -> BookmarkItemRecord {
216 BookmarkItemRecord::Query(q)
217 }
218}
219
220#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
221#[serde(rename_all = "camelCase")]
222pub struct FolderRecord {
223 #[serde(rename = "id")]
224 pub record_id: BookmarkRecordId,
225
226 #[serde(rename = "parentid")]
227 pub parent_record_id: Option<BookmarkRecordId>,
228
229 #[serde(rename = "parentName", skip_serializing_if = "Option::is_none")]
230 pub parent_title: Option<String>,
231
232 #[serde(skip_serializing_if = "Option::is_none")]
233 #[serde(default, deserialize_with = "de_maybe_stringified_timestamp")]
234 pub date_added: Option<i64>,
235
236 #[serde(default)]
237 pub has_dupe: bool,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub title: Option<String>,
241
242 #[serde(default)]
243 pub children: Vec<BookmarkRecordId>,
244
245 #[serde(flatten)]
246 pub unknown_fields: UnknownFields,
247}
248
249impl From<FolderRecord> for BookmarkItemRecord {
250 #[inline]
251 fn from(f: FolderRecord) -> BookmarkItemRecord {
252 BookmarkItemRecord::Folder(f)
253 }
254}
255
256#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
257#[serde(rename_all = "camelCase")]
258pub struct LivemarkRecord {
259 #[serde(rename = "id")]
260 pub record_id: BookmarkRecordId,
261
262 #[serde(rename = "parentid")]
263 pub parent_record_id: Option<BookmarkRecordId>,
264
265 #[serde(rename = "parentName", skip_serializing_if = "Option::is_none")]
266 pub parent_title: Option<String>,
267
268 #[serde(skip_serializing_if = "Option::is_none")]
269 #[serde(default, deserialize_with = "de_maybe_stringified_timestamp")]
270 pub date_added: Option<i64>,
271
272 #[serde(default)]
273 pub has_dupe: bool,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub title: Option<String>,
277
278 #[serde(rename = "feedUri", skip_serializing_if = "Option::is_none")]
279 pub feed_url: Option<String>,
280
281 #[serde(rename = "siteUri", skip_serializing_if = "Option::is_none")]
282 pub site_url: Option<String>,
283
284 #[serde(flatten)]
285 pub unknown_fields: UnknownFields,
286}
287
288impl From<LivemarkRecord> for BookmarkItemRecord {
289 #[inline]
290 fn from(l: LivemarkRecord) -> BookmarkItemRecord {
291 BookmarkItemRecord::Livemark(l)
292 }
293}
294
295#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
296#[serde(rename_all = "camelCase")]
297pub struct SeparatorRecord {
298 #[serde(rename = "id")]
299 pub record_id: BookmarkRecordId,
300
301 #[serde(rename = "parentid")]
302 pub parent_record_id: Option<BookmarkRecordId>,
303
304 #[serde(rename = "parentName", skip_serializing_if = "Option::is_none")]
305 pub parent_title: Option<String>,
306
307 #[serde(skip_serializing_if = "Option::is_none")]
308 #[serde(default, deserialize_with = "de_maybe_stringified_timestamp")]
309 pub date_added: Option<i64>,
310
311 #[serde(default)]
312 pub has_dupe: bool,
313
314 #[serde(rename = "pos", skip_serializing_if = "Option::is_none")]
317 pub position: Option<i64>,
318
319 #[serde(flatten)]
320 pub unknown_fields: UnknownFields,
321}
322
323impl From<SeparatorRecord> for BookmarkItemRecord {
324 #[inline]
325 fn from(s: SeparatorRecord) -> BookmarkItemRecord {
326 BookmarkItemRecord::Separator(s)
327 }
328}
329
330#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
331#[serde(tag = "type", rename_all = "camelCase")]
332pub enum BookmarkItemRecord {
333 Bookmark(BookmarkRecord),
334 Query(QueryRecord),
335 Folder(FolderRecord),
336 Livemark(LivemarkRecord),
337 Separator(SeparatorRecord),
338}
339
340impl BookmarkItemRecord {
341 pub fn record_id(&self) -> &BookmarkRecordId {
342 match self {
343 Self::Bookmark(b) => &b.record_id,
344 Self::Query(q) => &q.record_id,
345 Self::Folder(f) => &f.record_id,
346 Self::Livemark(l) => &l.record_id,
347 Self::Separator(s) => &s.record_id,
348 }
349 }
350
351 pub fn unknown_fields(&self) -> &UnknownFields {
352 match self {
353 Self::Bookmark(b) => &b.unknown_fields,
354 Self::Folder(f) => &f.unknown_fields,
355 Self::Separator(s) => &s.unknown_fields,
356 Self::Query(q) => &q.unknown_fields,
357 Self::Livemark(l) => &l.unknown_fields,
358 }
359 }
360}
361
362fn de_maybe_stringified_timestamp<'de, D>(
364 deserializer: D,
365) -> std::result::Result<Option<i64>, D::Error>
366where
367 D: serde::de::Deserializer<'de>,
368{
369 use std::marker::PhantomData;
370
371 struct StringOrInt(PhantomData<Option<i64>>);
372
373 impl Visitor<'_> for StringOrInt {
374 type Value = Option<i64>;
375
376 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
377 formatter.write_str("string or int")
378 }
379
380 fn visit_str<E>(self, value: &str) -> Result<Option<i64>, E>
381 where
382 E: serde::de::Error,
383 {
384 match value.parse::<i64>() {
385 Ok(v) => Ok(Some(v)),
386 Err(_) => Err(E::custom("invalid string literal")),
387 }
388 }
389
390 fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Option<i64>, E> {
392 Ok(Some(value.max(0)))
393 }
394
395 fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Option<i64>, E> {
397 Ok(Some((value as i64).max(0)))
398 }
399 }
400 deserializer.deserialize_any(StringOrInt(PhantomData))
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use serde_json::{json, Error};
407
408 #[test]
409 fn test_invalid_record_type() {
410 let r: std::result::Result<BookmarkItemRecord, Error> =
411 serde_json::from_value(json!({"id": "whatever", "type" : "unknown-type"}));
412 let e = r.unwrap_err();
413 assert!(e.is_data());
414 assert!(e.to_string().contains("unknown-type"));
416 }
417
418 #[test]
419 fn test_id_rewriting() {
420 let j = json!({"id": "unfiled", "parentid": "menu", "type": "bookmark"});
421 let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
422 match &r {
423 BookmarkItemRecord::Bookmark(b) => {
424 assert_eq!(b.record_id.as_guid(), BookmarkRootGuid::Unfiled);
425 assert_eq!(
426 b.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
427 Some(&BookmarkRootGuid::Menu.as_guid())
428 );
429 }
430 _ => panic!("unexpected record type"),
431 };
432 let v = serde_json::to_value(r).expect("should serialize");
433 assert_eq!(
434 v,
435 json!({
436 "id": "unfiled",
437 "parentid": "menu",
438 "type": "bookmark",
439 "hasDupe": false,
440 })
441 );
442
443 let j = json!({"id": "unfiled", "parentid": "menu", "type": "query"});
444 let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
445 match &r {
446 BookmarkItemRecord::Query(q) => {
447 assert_eq!(q.record_id.as_guid(), BookmarkRootGuid::Unfiled);
448 assert_eq!(
449 q.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
450 Some(&BookmarkRootGuid::Menu.as_guid())
451 );
452 }
453 _ => panic!("unexpected record type"),
454 };
455 let v = serde_json::to_value(r).expect("should serialize");
456 assert_eq!(
457 v,
458 json!({
459 "id": "unfiled",
460 "parentid": "menu",
461 "type": "query",
462 "hasDupe": false,
463 })
464 );
465
466 let j = json!({"id": "unfiled", "parentid": "menu", "type": "folder"});
467 let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
468 match &r {
469 BookmarkItemRecord::Folder(f) => {
470 assert_eq!(f.record_id.as_guid(), BookmarkRootGuid::Unfiled);
471 assert_eq!(
472 f.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
473 Some(&BookmarkRootGuid::Menu.as_guid())
474 );
475 }
476 _ => panic!("unexpected record type"),
477 };
478 let v = serde_json::to_value(r).expect("should serialize");
479 assert_eq!(
480 v,
481 json!({
482 "id": "unfiled",
483 "parentid": "menu",
484 "type": "folder",
485 "hasDupe": false,
486 "children": [],
487 })
488 );
489
490 let j = json!({"id": "unfiled", "parentid": "menu", "type": "livemark"});
491 let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
492 match &r {
493 BookmarkItemRecord::Livemark(l) => {
494 assert_eq!(l.record_id.as_guid(), BookmarkRootGuid::Unfiled);
495 assert_eq!(
496 l.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
497 Some(&BookmarkRootGuid::Menu.as_guid())
498 );
499 }
500 _ => panic!("unexpected record type"),
501 };
502 let v = serde_json::to_value(r).expect("should serialize");
503 assert_eq!(
504 v,
505 json!({
506 "id": "unfiled",
507 "parentid": "menu",
508 "type": "livemark",
509 "hasDupe": false,
510 })
511 );
512
513 let j = json!({"id": "unfiled", "parentid": "menu", "type": "separator"});
514 let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
515 match &r {
516 BookmarkItemRecord::Separator(s) => {
517 assert_eq!(s.record_id.as_guid(), BookmarkRootGuid::Unfiled);
518 assert_eq!(
519 s.parent_record_id.as_ref().map(BookmarkRecordId::as_guid),
520 Some(&BookmarkRootGuid::Menu.as_guid())
521 );
522 }
523 _ => panic!("unexpected record type"),
524 };
525 let v = serde_json::to_value(r).expect("should serialize");
526 assert_eq!(
527 v,
528 json!({
529 "id": "unfiled",
530 "parentid": "menu",
531 "type": "separator",
532 "hasDupe": false,
533 })
534 );
535 }
536
537 fn check_date_added(j: serde_json::Value, expected: Option<i64>) {
543 let r: BookmarkItemRecord = serde_json::from_value(j).expect("should deserialize");
544 match &r {
545 BookmarkItemRecord::Bookmark(b) => assert_eq!(b.date_added, expected),
546 _ => panic!("unexpected record type"),
547 };
548 }
549
550 #[test]
551 fn test_dateadded_missing() {
552 check_date_added(
553 json!({"id": "unfiled", "parentid": "menu", "type": "bookmark"}),
554 None,
555 )
556 }
557
558 #[test]
559 fn test_dateadded_int() {
560 check_date_added(
561 json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": 123}),
562 Some(123),
563 )
564 }
565
566 #[test]
567 fn test_dateadded_negative() {
568 check_date_added(
569 json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": -1}),
570 Some(0),
571 )
572 }
573
574 #[test]
575 fn test_dateadded_str() {
576 check_date_added(
577 json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": "123"}),
578 Some(123),
579 )
580 }
581
582 #[test]
584 fn test_dateadded_null() {
585 serde_json::from_value::<BookmarkItemRecord>(
587 json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": null}),
588 )
589 .expect_err("should fail, literal null");
590 }
591
592 #[test]
593 fn test_dateadded_invalid_str() {
594 serde_json::from_value::<BookmarkItemRecord>(
595 json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": "foo"}),
596 )
597 .expect_err("should fail, bad string value");
598 }
599
600 #[test]
601 fn test_dateadded_invalid_type() {
602 serde_json::from_value::<BookmarkItemRecord>(
603 json!({"id": "unfiled", "parentid": "menu", "type": "bookmark", "dateAdded": []}),
604 )
605 .expect_err("should fail, invalid type");
606 }
607}