remote_settings/
config.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
5//! This module defines the custom configurations that consumers can set.
6//! Those configurations override default values and can be used to set a custom server,
7//! collection name, and bucket name.
8//! The purpose of the configuration parameters are to allow consumers an easy debugging option,
9//! and the ability to be explicit about the server.
10
11use url::Url;
12
13use crate::error::warn;
14use crate::{ApiResult, Error, RemoteSettingsContext, Result};
15
16/// Remote settings configuration
17///
18/// This is the version used in the new API, hence the `2` at the end.  The plan is to move
19/// consumers to the new API, remove the RemoteSettingsConfig struct, then remove the `2` from this
20/// name.
21#[derive(Debug, Default, Clone, uniffi::Record)]
22pub struct RemoteSettingsConfig2 {
23    /// The Remote Settings server to use. Defaults to [RemoteSettingsServer::Prod],
24    #[uniffi(default = None)]
25    pub server: Option<RemoteSettingsServer>,
26    /// Bucket name to use, defaults to "main".  Use "main-preview" for a preview bucket
27    #[uniffi(default = None)]
28    pub bucket_name: Option<String>,
29    /// App context to use for JEXL filtering (when the `jexl` feature is present).
30    #[uniffi(default = None)]
31    pub app_context: Option<RemoteSettingsContext>,
32}
33
34/// Custom configuration for the client.
35/// Currently includes the following:
36/// - `server`: The Remote Settings server to use. If not specified, defaults to the production server (`RemoteSettingsServer::Prod`).
37/// - `server_url`: An optional custom Remote Settings server URL. Deprecated; please use `server` instead.
38/// - `bucket_name`: The optional name of the bucket containing the collection on the server. If not specified, the standard bucket will be used.
39/// - `collection_name`: The name of the collection for the settings server.
40#[derive(Debug, Clone, uniffi::Record)]
41pub struct RemoteSettingsConfig {
42    pub collection_name: String,
43    #[uniffi(default = None)]
44    pub bucket_name: Option<String>,
45    #[uniffi(default = None)]
46    pub server_url: Option<String>,
47    #[uniffi(default = None)]
48    pub server: Option<RemoteSettingsServer>,
49}
50
51/// The Remote Settings server that the client should use.
52#[derive(Debug, Clone, uniffi::Enum)]
53pub enum RemoteSettingsServer {
54    Prod,
55    Stage,
56    Dev,
57    Custom { url: String },
58}
59
60impl RemoteSettingsServer {
61    /// Get the [url::Url] for this server
62    #[error_support::handle_error(Error)]
63    pub fn url(&self) -> ApiResult<Url> {
64        self.get_url()
65    }
66
67    /// Get a BaseUrl for this server
68    pub fn get_base_url(&self) -> Result<BaseUrl> {
69        let base_url = BaseUrl::parse(self.raw_url())?;
70        // Custom URLs are weird and require a couple tricks for backwards compatibility.
71        // Normally we append `v1/` to match how this has historically worked.  However,
72        // don't do this for file:// schemes which normally don't make any sense, but it's
73        // what Nimbus uses to indicate they want to use the file-based client, rather than
74        // a remote-settings based one.
75        if base_url.url().scheme() != "file" {
76            Ok(base_url.join("v1"))
77        } else {
78            Ok(base_url)
79        }
80    }
81
82    /// get_url() that never fails
83    ///
84    /// If the URL is invalid, we'll log a warning and fall back to the production URL
85    pub fn get_base_url_with_prod_fallback(&self) -> BaseUrl {
86        match self.get_base_url() {
87            Ok(url) => url,
88            // The unwrap below will never fail, since prod is a hard-coded/valid URL.
89            Err(_) => {
90                warn!("Invalid Custom URL: {}", self.raw_url());
91                BaseUrl::parse(Self::Prod.raw_url()).unwrap()
92            }
93        }
94    }
95
96    fn raw_url(&self) -> &str {
97        match self {
98            Self::Prod => "https://firefox.settings.services.mozilla.com/v1",
99            Self::Stage => "https://firefox.settings.services.allizom.org/v1",
100            Self::Dev => "https://remote-settings-dev.allizom.org/v1",
101            Self::Custom { url } => url,
102        }
103    }
104
105    /// Internal version of `url()`.
106    ///
107    /// The difference is that it uses `Error` instead of `ApiError`.  This is what we need to use
108    /// inside the crate.
109    pub fn get_url(&self) -> Result<Url> {
110        Ok(match self {
111            Self::Prod => Url::parse("https://firefox.settings.services.mozilla.com/v1")?,
112            Self::Stage => Url::parse("https://firefox.settings.services.allizom.org/v1")?,
113            Self::Dev => Url::parse("https://remote-settings-dev.allizom.org/v1")?,
114            Self::Custom { url } => {
115                let mut url = Url::parse(url)?;
116                // Custom URLs are weird and require a couple tricks for backwards compatibility.
117                // Normally we append `v1/` to match how this has historically worked.  However,
118                // don't do this for file:// schemes which normally don't make any sense, but it's
119                // what Nimbus uses to indicate they want to use the file-based client, rather than
120                // a remote-settings based one.
121                if url.scheme() != "file" {
122                    url = url.join("v1")?
123                }
124                url
125            }
126        })
127    }
128}
129
130/// Url that's guaranteed safe to use as a base
131#[derive(Debug, Clone)]
132pub struct BaseUrl {
133    url: Url,
134}
135
136impl BaseUrl {
137    pub fn parse(url: &str) -> Result<Self> {
138        let url = Url::parse(url)?;
139        if url.cannot_be_a_base() {
140            Err(Error::UrlParsingError(
141                url::ParseError::RelativeUrlWithCannotBeABaseBase,
142            ))
143        } else {
144            Ok(Self { url })
145        }
146    }
147
148    pub fn url(&self) -> &Url {
149        &self.url
150    }
151
152    pub fn into_inner(self) -> Url {
153        self.url
154    }
155
156    pub fn join(&self, input: &str) -> BaseUrl {
157        Self {
158            // Unwrap is safe, because the join() docs say that it only will error for
159            // cannot-be-a-base URLs.
160            url: self.url.join(input).unwrap(),
161        }
162    }
163
164    pub fn path_segments_mut(&mut self) -> url::PathSegmentsMut<'_> {
165        // Unwrap is safe, because the path_segments_mut() docs say that it only will
166        // error for cannot-be-a-base URLs.
167        self.url.path_segments_mut().unwrap()
168    }
169
170    pub fn query_pairs_mut(&mut self) -> url::form_urlencoded::Serializer<'_, url::UrlQuery<'_>> {
171        self.url.query_pairs_mut()
172    }
173}