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}