remote_settings/context.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_json::{Map, Value};
6use std::collections::HashMap;
7
8/// Remote settings context object
9///
10/// This is used to filter the records returned. We always fetch all `records` from the
11/// remote-settings storage. Some records could have a `filter_expression`. If this is passed in
12/// and the record has a `filter_expression`, then only returns where the expression is true will
13/// be returned.
14///
15/// See https://remote-settings.readthedocs.io/en/latest/target-filters.html for details.
16#[derive(Debug, Clone, Default, uniffi::Record)]
17pub struct RemoteSettingsContext {
18 /// The delivery channel of the application (e.g "nightly")
19 #[uniffi(default = None)]
20 pub channel: Option<String>,
21 /// User visible version string (e.g. "1.0.3")
22 #[uniffi(default = None)]
23 pub app_version: Option<String>,
24 /// String containing the XUL application app_id
25 #[uniffi(default = None)]
26 pub app_id: Option<String>,
27 /// The locale of the application during initialization (e.g. "es-ES")
28 #[uniffi(default = None)]
29 pub locale: Option<String>,
30 /// The name of the operating system (e.g. "Android", "iOS", "Darwin", "WINNT")
31 #[uniffi(default = None)]
32 pub os: Option<String>,
33 /// The user-visible version of the operating system (e.g. "1.2.3")
34 #[uniffi(default = None)]
35 pub os_version: Option<String>,
36 /// Form-factor of the device ("phone", "tablet", or "desktop")
37 #[uniffi(default = None)]
38 pub form_factor: Option<String>,
39 /// Country of the user.
40 ///
41 /// This is usually populated in one of two ways:
42 /// - The second component of the locale
43 /// - By using a geolocation service, which determines country via the user's IP.
44 /// Firefox apps usually have a module that integrates with these services,
45 /// for example `Region` on Desktop and `RegionMiddleware` on Android.
46 #[uniffi(default = None)]
47 pub country: Option<String>,
48 /// Extra attributes to add to the env for JEXL filtering.
49 ///
50 /// Use this for prototyping / testing new features. In the long-term, new fields should be
51 /// added to the official list and supported by both the Rust and Gecko clients.
52 #[uniffi(default = None)]
53 pub custom_targetting_attributes: Option<HashMap<String, String>>,
54}
55
56impl RemoteSettingsContext {
57 /// Convert this into the `env` value for the remote settings JEXL filter
58 ///
59 /// https://remote-settings.readthedocs.io/en/latest/target-filters.html
60 pub(crate) fn into_env(self) -> Value {
61 let mut v = Map::new();
62 v.insert("channel".to_string(), self.channel.into());
63 if let Some(version) = self.app_version {
64 v.insert("version".to_string(), version.into());
65 }
66 if let Some(locale) = self.locale {
67 v.insert("locale".to_string(), locale.into());
68 }
69 if self.app_id.is_some() || self.os.is_some() || self.os_version.is_some() {
70 let mut appinfo = Map::default();
71 if let Some(app_id) = self.app_id {
72 appinfo.insert("ID".to_string(), app_id.into());
73 }
74 // The "os" object is the new way to represent OS-related data
75 if self.os.is_some() || self.os_version.is_some() {
76 let mut os = Map::default();
77 if let Some(os_name) = &self.os {
78 os.insert("name".to_string(), os_name.to_string().into());
79 }
80 if let Some(os_version) = self.os_version {
81 os.insert("version".to_string(), os_version.into());
82 }
83 appinfo.insert("os".to_string(), os.into());
84 }
85 // The "OS" string is for backwards compatibility
86 if let Some(os_name) = self.os {
87 appinfo.insert("OS".to_string(), os_name.into());
88 }
89 v.insert("appinfo".to_string(), appinfo.into());
90 }
91 if let Some(form_factor) = self.form_factor {
92 v.insert("formFactor".to_string(), form_factor.into());
93 }
94 if let Some(country) = self.country {
95 v.insert("country".to_string(), country.into());
96 }
97 if let Some(custom) = self.custom_targetting_attributes {
98 v.extend(custom.into_iter().map(|(k, v)| (k, v.into())));
99 }
100 v.into()
101 }
102}
103
104#[cfg(test)]
105mod test {
106 use super::*;
107 use serde_json::json;
108
109 /// Test that the remote settings context is normalized to match
110 /// https://remote-settings.readthedocs.io/en/latest/target-filters.html, regardless of what
111 /// the fields are named in Rust.
112 #[test]
113 fn test_context_normalization() {
114 let context = RemoteSettingsContext {
115 channel: Some("beta".into()),
116 app_id: Some("{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}".into()),
117 app_version: Some("1.0.0".into()),
118 os: Some("MS-DOS".into()),
119 os_version: Some("6.1".into()),
120 locale: Some("en-US".into()),
121 form_factor: Some("tablet".into()),
122 country: Some("US".into()),
123 custom_targetting_attributes: Some(HashMap::from([("extra".into(), "test".into())])),
124 };
125 assert_eq!(
126 context.into_env(),
127 json!({
128 // Official fields
129 "version": "1.0.0",
130 "channel": "beta",
131 "locale": "en-US",
132 "appinfo": {
133 "ID": "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}",
134 "OS": "MS-DOS",
135 "os": {
136 "name": "MS-DOS",
137 "version": "6.1"
138 }
139 },
140 // Unofficial fields that we need for Suggest geo-expansion. These should be made
141 // into official fields that both the Gecko and Rust client support.
142 "formFactor": "tablet",
143 "country": "US",
144 "extra": "test",
145 })
146 );
147 }
148}