remote_settings/
cache.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 crate::RemoteSettingsResponse;
6use std::collections::HashSet;
7
8/// Merge a cached RemoteSettingsResponse and a newly downloaded one to get a merged response
9///
10/// cached is a previously downloaded remote settings response (possibly run through merge_cache_and_response).
11/// new is a newly downloaded remote settings response (with `_expected` set to the last_modified
12/// time of the cached response).
13///
14/// This will merge the records from both responses, handle deletions/tombstones, and return a
15/// response that has:
16///   - The newest `last_modified_date`
17///   - A record list containing the newest version of all live records.  Deleted records will not
18///     be present in this list.
19///
20/// If everything is working properly, the returned value will exactly match what the server would
21/// have returned if there was no `_expected` param.
22pub fn merge_cache_and_response(
23    cached: RemoteSettingsResponse,
24    new: RemoteSettingsResponse,
25) -> RemoteSettingsResponse {
26    let new_record_ids = new
27        .records
28        .iter()
29        .map(|r| r.id.as_str())
30        .collect::<HashSet<&str>>();
31    // Start with any cached records that don't appear in new.
32    let mut records = cached
33        .records
34        .into_iter()
35        .filter(|r| !new_record_ids.contains(r.id.as_str()))
36        // deleted should always be false, check it just in case
37        .filter(|r| !r.deleted)
38        .collect::<Vec<_>>();
39    // Add all (non-deleted) records from new
40    records.extend(new.records.into_iter().filter(|r| !r.deleted));
41
42    RemoteSettingsResponse {
43        last_modified: new.last_modified,
44        records,
45    }
46}
47
48#[cfg(test)]
49mod test {
50    use super::*;
51    use crate::{RemoteSettingsRecord, RsJsonObject};
52
53    // Quick way to generate the fields data for our mock records
54    fn fields(data: &str) -> RsJsonObject {
55        let mut map = serde_json::Map::new();
56        map.insert("data".into(), data.into());
57        map
58    }
59
60    #[test]
61    fn test_combine_cache_and_response() {
62        let cached_response = RemoteSettingsResponse {
63            last_modified: 1000,
64            records: vec![
65                RemoteSettingsRecord {
66                    id: "a".into(),
67                    last_modified: 100,
68                    deleted: false,
69                    attachment: None,
70                    fields: fields("a"),
71                },
72                RemoteSettingsRecord {
73                    id: "b".into(),
74                    last_modified: 200,
75                    deleted: false,
76                    attachment: None,
77                    fields: fields("b"),
78                },
79                RemoteSettingsRecord {
80                    id: "c".into(),
81                    last_modified: 300,
82                    deleted: false,
83                    attachment: None,
84                    fields: fields("c"),
85                },
86            ],
87        };
88        let new_response = RemoteSettingsResponse {
89            last_modified: 2000,
90            records: vec![
91                // d is new
92                RemoteSettingsRecord {
93                    id: "d".into(),
94                    last_modified: 1300,
95                    deleted: false,
96                    attachment: None,
97                    fields: fields("d"),
98                },
99                // b was deleted
100                RemoteSettingsRecord {
101                    id: "b".into(),
102                    last_modified: 1200,
103                    deleted: true,
104                    attachment: None,
105                    fields: RsJsonObject::new(),
106                },
107                // a was updated
108                RemoteSettingsRecord {
109                    id: "a".into(),
110                    last_modified: 1100,
111                    deleted: false,
112                    attachment: None,
113                    fields: fields("a-with-new-data"),
114                },
115                // c was not modified, so it's not present in the new response
116            ],
117        };
118        let mut merged = merge_cache_and_response(cached_response, new_response);
119        // Sort the records to make the assertion easier
120        merged.records.sort_by_key(|r| r.id.clone());
121        assert_eq!(
122            merged,
123            RemoteSettingsResponse {
124                last_modified: 2000,
125                records: vec![
126                    // a was updated
127                    RemoteSettingsRecord {
128                        id: "a".into(),
129                        last_modified: 1100,
130                        deleted: false,
131                        attachment: None,
132                        fields: fields("a-with-new-data"),
133                    },
134                    RemoteSettingsRecord {
135                        id: "c".into(),
136                        last_modified: 300,
137                        deleted: false,
138                        attachment: None,
139                        fields: fields("c"),
140                    },
141                    RemoteSettingsRecord {
142                        id: "d".into(),
143                        last_modified: 1300,
144                        deleted: false,
145                        attachment: None,
146                        fields: fields("d"),
147                    },
148                ],
149            }
150        );
151    }
152}