tabs/sync/
record.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use serde_derive::{Deserialize, Serialize};
6
7// copy/pasta...
8fn skip_if_default<T: PartialEq + Default>(v: &T) -> bool {
9    *v == T::default()
10}
11
12#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
13#[serde(rename_all = "camelCase")]
14pub struct TabsRecordTab {
15    pub title: String,
16    pub url_history: Vec<String>,
17    pub icon: Option<String>,
18    pub last_used: i64, // Seconds since epoch!
19    #[serde(default, skip_serializing_if = "skip_if_default")]
20    pub inactive: bool,
21}
22
23#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25// This struct mirrors what is stored on the server
26pub struct TabsRecord {
27    // `String` instead of `SyncGuid` because some IDs are FxA device ID (XXX - that doesn't
28    // matter though - this could easily be a Guid!)
29    pub id: String,
30    pub client_name: String,
31    pub tabs: Vec<TabsRecordTab>,
32}
33
34#[cfg(test)]
35pub mod test {
36    use super::*;
37    use serde_json::json;
38
39    #[test]
40    fn test_payload() {
41        let payload = json!({
42            "id": "JkeBPC50ZI0m",
43            "clientName": "client name",
44            "tabs": [{
45                "title": "the title",
46                "urlHistory": [
47                    "https://mozilla.org/"
48                ],
49                "icon": "https://mozilla.org/icon",
50                "lastUsed": 1643764207
51            }]
52        });
53        let record: TabsRecord = serde_json::from_value(payload).expect("should work");
54        assert_eq!(record.id, "JkeBPC50ZI0m");
55        assert_eq!(record.client_name, "client name");
56        assert_eq!(record.tabs.len(), 1);
57        let tab = &record.tabs[0];
58        assert_eq!(tab.title, "the title");
59        assert_eq!(tab.icon, Some("https://mozilla.org/icon".to_string()));
60        assert_eq!(tab.last_used, 1643764207);
61        assert!(!tab.inactive);
62    }
63
64    #[test]
65    fn test_roundtrip() {
66        let tab = TabsRecord {
67            id: "JkeBPC50ZI0m".into(),
68            client_name: "client name".into(),
69            tabs: vec![TabsRecordTab {
70                title: "the title".into(),
71                url_history: vec!["https://mozilla.org/".into()],
72                icon: Some("https://mozilla.org/icon".into()),
73                last_used: 1643764207,
74                inactive: true,
75            }],
76        };
77        let round_tripped =
78            serde_json::from_value(serde_json::to_value(tab.clone()).unwrap()).unwrap();
79        assert_eq!(tab, round_tripped);
80    }
81
82    #[test]
83    fn test_extra_fields() {
84        let payload = json!({
85            "id": "JkeBPC50ZI0m",
86            // Let's say we agree on new tabs to record, we want old versions to
87            // ignore them!
88            "ignoredField": "??",
89            "clientName": "client name",
90            "tabs": [{
91                "title": "the title",
92                "urlHistory": [
93                    "https://mozilla.org/"
94                ],
95                "icon": "https://mozilla.org/icon",
96                "lastUsed": 1643764207,
97                // Ditto - make sure we ignore unexpected fields in each tab.
98                "ignoredField": "??",
99            }]
100        });
101        let record: TabsRecord = serde_json::from_value(payload).unwrap();
102        // The point of this test is really just to ensure the deser worked, so
103        // just check the ID.
104        assert_eq!(record.id, "JkeBPC50ZI0m");
105    }
106}