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}