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