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