/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::{collections::HashMap, fs::File, io::prelude::Write, sync::Arc};
use error_support::{convert_log_report_error, handle_error}; use url::Url;
use client::Client; use error::Error; use storage::Storage;
uniffi::setup_scaffolding!("remote_settings");
/// The `RemoteSettingsContext` object represents the parameters and characteristics of the /// consuming application. For `remote-settings`, it is used to filter locally stored `records`. /// /// We always fetch all `records` from the remote-settings storage. Some records could have a `filter_expression` /// attached to them, which will be matched against the `RemoteSettingsContext`. /// /// When set, only records where the expression is true will be returned. #[derive(Deserialize, Serialize, Debug, Clone, Default, uniffi::Record)] pubstruct RemoteSettingsContext { /// Name of the application (e.g. "Fenix" or "Firefox iOS") pub app_name: String, /// Application identifier, especially for mobile (e.g. "org.mozilla.fenix") pub app_id: String, /// The delivery channel of the application (e.g "nightly") pub channel: String, /// User visible version string (e.g. "1.0.3") #[serde(rename = "version")] pub app_version: Option<String>, /// Build identifier generated by the CI system (e.g. "1234/A") pub app_build: Option<String>, /// The architecture of the device, (e.g. "arm", "x86") pub architecture: Option<String>, /// The manufacturer of the device the application is running on pub device_manufacturer: Option<String>, /// The model of the device the application is running on pub device_model: Option<String>, /// The locale of the application during initialization (e.g. "es-ES") pub locale: Option<String>, /// The name of the operating system (e.g. "Android", "iOS", "Darwin", "Windows") pub os: Option<String>, /// The user-visible version of the operating system (e.g. "1.2.3") pub os_version: Option<String>, /// Android specific for targeting specific sdk versions pub android_sdk_version: Option<String>, /// Used for debug purposes as a way to match only developer builds, etc. pub debug_tag: Option<String>, /// The date the application installed the app pub installation_date: Option<i64>, /// The application's home directory pub home_directory: Option<String>, /// Contains attributes specific to the application, derived by the application #[serde(flatten)] pub custom_targeting_attributes: Option<Map<String, Value>>,
}
/// Application-level Remote Settings manager. /// /// This handles application-level operations, like syncing all the collections, and acts as a /// factory for creating clients. #[derive(uniffi::Object)] pubstruct RemoteSettingsService { // This struct adapts server::RemoteSettingsService into the public API
internal: service::RemoteSettingsService,
}
#[uniffi::export] impl RemoteSettingsService { /// Construct a [RemoteSettingsService] /// /// This is typically done early in the application-startup process #[uniffi::constructor] #[handle_error(Error)] pubfn new(storage_dir: String, config: RemoteSettingsConfig2) -> ApiResult<Self> {
Ok(Self {
internal: service::RemoteSettingsService::new(storage_dir, config)?,
})
}
/// Sync collections for all active clients #[handle_error(Error)] pubfn sync(&self) -> ApiResult<Vec<String>> { self.internal.sync()
}
/// Update the remote settings config /// /// This will cause all current and future clients to use new config and will delete any stored /// records causing the clients to return new results from the new config. /// /// Only intended for QA/debugging. Swapping the remote settings server in the middle of /// execution can cause weird effects. #[handle_error(Error)] pubfn update_config(&self, config: RemoteSettingsConfig2) -> ApiResult<()> { self.internal.update_config(config)
}
}
/// Client for a single Remote Settings collection /// /// Use [RemoteSettingsService::make_client] to create these. #[derive(uniffi::Object)] pubstruct RemoteSettingsClient { // This struct adapts client::RemoteSettingsClient into the public API
internal: client::RemoteSettingsClient,
}
#[uniffi::export] impl RemoteSettingsClient { /// Collection this client is for pubfn collection_name(&self) -> String { self.internal.collection_name().to_owned()
}
/// Get the current set of records. /// /// This method normally fetches records from the last sync. This means that it returns fast /// and does not make any network requests. /// /// If records have not yet been synced it will return None. Use `sync_if_empty = true` to /// change this behavior and perform a network request in this case. That this is probably a /// bad idea if you want to fetch the setting in application startup or when building the UI. /// /// None will also be returned on disk IO errors or other unexpected errors. The reason for /// this is that there is not much an application can do in this situation other than fall back /// to the same default handling as if records have not been synced. /// /// Application-services schedules regular dumps of the server data for specific collections. /// For these collections, `get_records` will never return None. If you would like to add your /// collection to this list, please reach out to the DISCO team. #[uniffi::method(default(sync_if_empty = false))] pubfn get_records(&self, sync_if_empty: bool) -> Option<Vec<RemoteSettingsRecord>> { matchself.internal.get_records(sync_if_empty) {
Ok(records) => records,
Err(e) => { // Log/report the error
log::trace!("get_records error: {e}");
convert_log_report_error(e); // Throw away the converted result and return None, there's nothing a client can // really do with an error except treat it as the None case
None
}
}
}
/// Get the current set of records as a map of record_id -> record. /// /// See [Self::get_records] for an explanation of when this makes network requests, error /// handling, and how the `sync_if_empty` param works. #[uniffi::method(default(sync_if_empty = false))] pubfn get_records_map(
&self,
sync_if_empty: bool,
) -> Option<HashMap<String, RemoteSettingsRecord>> { self.get_records(sync_if_empty)
.map(|records| records.into_iter().map(|r| (r.id.clone(), r)).collect())
}
/// Get attachment data for a remote settings record /// /// Attachments are large binary blobs used for data that doesn't fit in a normal record. They /// are handled differently than other record data: /// /// - Attachments are not downloaded in [RemoteSettingsService::sync] /// - This method will make network requests if the attachment is not cached /// - This method will throw if there is a network or other error when fetching the /// attachment data. #[handle_error(Error)] pubfn get_attachment(&self, record: RemoteSettingsRecord) -> ApiResult<Vec<u8>> { self.internal.get_attachment(record)
}
}
impl RemoteSettingsClient { /// Create a new client. This is not exposed to foreign code, consumers need to call /// [RemoteSettingsService::make_client] fn new(
base_url: Url,
bucket_name: String,
collection_name: String, #[cfg(feature = "jexl")] context: Option<RemoteSettingsContext>,
storage: Storage,
) -> Result<Self> {
Ok(Self {
internal: client::RemoteSettingsClient::new(
base_url,
bucket_name,
collection_name, #[cfg(feature = "jexl")]
context,
storage,
)?,
})
}
}
#[uniffi::export] impl RemoteSettings { /// Construct a new Remote Settings client with the given configuration. #[uniffi::constructor] #[handle_error(Error)] pubfn new(remote_settings_config: RemoteSettingsConfig) -> ApiResult<Self> {
Ok(RemoteSettings {
config: remote_settings_config.clone(),
client: Client::new(remote_settings_config)?,
})
}
/// Fetch all records for the configuration this client was initialized with. #[handle_error(Error)] pubfn get_records(&self) -> ApiResult<RemoteSettingsResponse> { let resp = self.client.get_records()?;
Ok(resp)
}
/// Fetch all records added to the server since the provided timestamp, /// using the configuration this client was initialized with. #[handle_error(Error)] pubfn get_records_since(&self, timestamp: u64) -> ApiResult<RemoteSettingsResponse> { let resp = self.client.get_records_since(timestamp)?;
Ok(resp)
}
/// Download an attachment with the provided id to the provided path. #[handle_error(Error)] pubfn download_attachment_to_path(
&self,
attachment_id: String,
path: String,
) -> ApiResult<()> { let resp = self.client.get_attachment(&attachment_id)?; letmut file = File::create(path)?;
file.write_all(&resp)?;
Ok(())
}
}
// Public functions that we don't expose via UniFFI. // // The long-term plan is to create a new remote settings client, transition nimbus + suggest to the // new API, then delete this code. impl RemoteSettings { /// Fetches all records for a collection that can be found in the server, /// bucket, and collection defined by the [ClientConfig] used to generate /// this [Client]. This function will return the raw viaduct [Response]. #[handle_error(Error)] pubfn get_records_raw(&self) -> ApiResult<viaduct::Response> { self.client.get_records_raw()
}
/// Downloads an attachment from [attachment_location]. NOTE: there are no /// guarantees about a maximum size, so use care when fetching potentially /// large attachments. #[handle_error(Error)] pubfn get_attachment(&self, attachment_location: &str) -> ApiResult<Vec<u8>> { self.client.get_attachment(attachment_location)
}
}
#[cfg(test)] mod test { usesuper::*; usecrate::RemoteSettingsRecord; use mockito::{mock, Matcher};
#[test] fn test_get_records() {
viaduct_reqwest::use_reqwest_backend(); let m = mock( "GET", "/v1/buckets/the-bucket/collections/the-collection/records",
)
.with_body(response_body())
.with_status(200)
.with_header("content-type", "application/json")
.with_header("etag", "\"1000\"")
.create();
let resp = remote_settings.get_records_since(500).unwrap();
assert!(are_equal_json(JPG_ATTACHMENT, &resp.records[0]));
assert_eq!(1000, resp.last_modified);
m.expect(1).assert();
}
// This test was designed as a proof-of-concept and requires a locally-run Remote Settings server. // If this were to be included in CI, it would require pulling the RS docker image and scripting // its configuration, as well as dynamically finding the attachment id, which would more closely // mimic a real world usecase. // #[test] #[allow(dead_code)] fn test_download() {
viaduct_reqwest::use_reqwest_backend(); let config = RemoteSettingsConfig {
server: Some(RemoteSettingsServer::Custom {
url: "http://localhost:8888".into(),
}),
server_url: None,
bucket_name: Some(String::from("the-bucket")),
collection_name: String::from("the-collection"),
}; let remote_settings = RemoteSettings::new(config).unwrap();
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.