Bug 1966784: Vendor application-services fa8a72a r=adw

Differential Revision: https://phabricator.services.mozilla.com/D249694
This commit is contained in:
dadaa
2025-05-16 02:52:01 +00:00
committed by dakatsuka.birchill@mozilla.com
parent 70e402a241
commit 8565135320
21 changed files with 652 additions and 209 deletions

View File

@@ -70,9 +70,9 @@ git = "https://github.com/jfkthame/mapped_hyph.git"
rev = "eff105f6ad7ec9b79816cfc1985a28e5340ad14b"
replace-with = "vendored-sources"
[source."git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197"]
[source."git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285"]
git = "https://github.com/mozilla/application-services"
rev = "40ae79e2825fa242a349e17fcfc84fb99fd92197"
rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
replace-with = "vendored-sources"
[source."git+https://github.com/mozilla/audioipc?rev=e6f44a2bd1e57d11dfc737632a9e849077632330"]

32
Cargo.lock generated
View File

@@ -1894,7 +1894,7 @@ dependencies = [
[[package]]
name = "error-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"error-support-macros",
"lazy_static",
@@ -1906,7 +1906,7 @@ dependencies = [
[[package]]
name = "error-support-macros"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"proc-macro2",
"quote",
@@ -2025,7 +2025,7 @@ dependencies = [
[[package]]
name = "firefox-versioning"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"serde_json",
"thiserror 1.999.999",
@@ -3365,7 +3365,7 @@ dependencies = [
[[package]]
name = "interrupt-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"lazy_static",
"parking_lot",
@@ -5086,7 +5086,7 @@ checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
[[package]]
name = "payload-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"serde",
"serde_derive",
@@ -5589,7 +5589,7 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "relevancy"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"anyhow",
"base64 0.21.999",
@@ -5614,7 +5614,7 @@ dependencies = [
[[package]]
name = "remote_settings"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"anyhow",
"camino",
@@ -5916,7 +5916,7 @@ dependencies = [
[[package]]
name = "search"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"error-support",
"firefox-versioning",
@@ -6208,7 +6208,7 @@ dependencies = [
[[package]]
name = "sql-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"interrupt-support",
"lazy_static",
@@ -6414,7 +6414,7 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "suggest"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"anyhow",
"chrono",
@@ -6466,7 +6466,7 @@ dependencies = [
[[package]]
name = "sync-guid"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"base64 0.21.999",
"rand",
@@ -6477,7 +6477,7 @@ dependencies = [
[[package]]
name = "sync15"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"anyhow",
"error-support",
@@ -6517,7 +6517,7 @@ dependencies = [
[[package]]
name = "tabs"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"anyhow",
"error-support",
@@ -6861,7 +6861,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "types"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"rusqlite 0.33.0",
"serde",
@@ -7264,7 +7264,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "viaduct"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"ffi-support",
"log",
@@ -7434,7 +7434,7 @@ dependencies = [
[[package]]
name = "webext-storage"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=40ae79e2825fa242a349e17fcfc84fb99fd92197#40ae79e2825fa242a349e17fcfc84fb99fd92197"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"anyhow",
"error-support",

View File

@@ -262,14 +262,14 @@ wr_malloc_size_of = { path = "gfx/wr/wr_malloc_size_of" }
objc = { git = "https://github.com/glandium/rust-objc", rev = "4de89f5aa9851ceca4d40e7ac1e2759410c04324" }
# application-services overrides to make updating them all simpler.
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "40ae79e2825fa242a349e17fcfc84fb99fd92197" }
relevancy = { git = "https://github.com/mozilla/application-services", rev = "40ae79e2825fa242a349e17fcfc84fb99fd92197" }
search = { git = "https://github.com/mozilla/application-services", rev = "40ae79e2825fa242a349e17fcfc84fb99fd92197" }
sql-support = { git = "https://github.com/mozilla/application-services", rev = "40ae79e2825fa242a349e17fcfc84fb99fd92197" }
suggest = { git = "https://github.com/mozilla/application-services", rev = "40ae79e2825fa242a349e17fcfc84fb99fd92197" }
sync15 = { git = "https://github.com/mozilla/application-services", rev = "40ae79e2825fa242a349e17fcfc84fb99fd92197" }
tabs = { git = "https://github.com/mozilla/application-services", rev = "40ae79e2825fa242a349e17fcfc84fb99fd92197" }
viaduct = { git = "https://github.com/mozilla/application-services", rev = "40ae79e2825fa242a349e17fcfc84fb99fd92197" }
webext-storage = { git = "https://github.com/mozilla/application-services", rev = "40ae79e2825fa242a349e17fcfc84fb99fd92197" }
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
relevancy = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
search = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
sql-support = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
suggest = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
sync15 = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
tabs = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
viaduct = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
webext-storage = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
allocator-api2 = { path = "third_party/rust/allocator-api2" }

View File

@@ -18,6 +18,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
YelpSubjectType: "resource://gre/modules/RustSuggest.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
@@ -960,6 +961,7 @@ class _QuickSuggestTestUtils {
isSuggestedIndexRelativeToGroup = true,
originalUrl = undefined,
displayUrl = undefined,
suggestedType = lazy.YelpSubjectType.SERVICE,
}) {
const utmParameters = "&utm_medium=partner&utm_source=mozilla";
@@ -1012,6 +1014,7 @@ class _QuickSuggestTestUtils {
0.2, // score
false, // hasLocationSign
false, // subjectExactMatch
suggestedType, // subjectType
"find_loc" // locationParam
);
}

View File

@@ -68,6 +68,10 @@
:members:
:exclude-members: SuggestionProvider
```
```{js:autoclass} RustSuggest.sys.YelpSubjectType
:members:
:exclude-members: YelpSubjectType
```
```{js:autoclass} RustSuggest.sys.Network
:members:
:exclude-members: Network

File diff suppressed because one or more lines are too long

View File

@@ -294,6 +294,12 @@ impl<C: ApiClient> RemoteSettingsClient<C> {
})
}
pub fn get_last_modified_timestamp(&self) -> Result<Option<u64>> {
let mut inner = self.inner.lock();
let collection_url = inner.api_client.collection_url();
inner.storage.get_last_modified_timestamp(&collection_url)
}
/// Synchronizes the local collection with the remote server by performing the following steps:
/// 1. Fetches the last modified timestamp of the collection from local storage.
/// 2. Fetches the changeset from the remote server based on the last modified timestamp.
@@ -507,9 +513,15 @@ impl RemoteSettingsClient<ViaductApiClient> {
Self::new_from_parts(collection_name, storage, jexl_filter, api_client)
}
pub fn update_config(&self, server_url: BaseUrl, bucket_name: String) -> Result<()> {
pub fn update_config(
&self,
server_url: BaseUrl,
bucket_name: String,
context: Option<RemoteSettingsContext>,
) -> Result<()> {
let mut inner = self.inner.lock();
inner.api_client = ViaductApiClient::new(server_url, &bucket_name, &self.collection_name);
inner.jexl_filter = JexlFilter::new(context);
inner.storage.empty()
}
}
@@ -554,12 +566,12 @@ impl ViaductApiClient {
fn make_request(&mut self, url: Url) -> Result<Response> {
log::trace!("make_request: {url}");
self.ensure_no_backoff()?;
self.remote_state.ensure_no_backoff()?;
let req = Request::get(url);
let resp = req.send()?;
self.handle_backoff_hint(&resp)?;
self.remote_state.handle_backoff_hint(&resp)?;
if resp.is_success() {
Ok(resp)
@@ -570,46 +582,6 @@ impl ViaductApiClient {
)))
}
}
fn ensure_no_backoff(&mut self) -> Result<()> {
if let BackoffState::Backoff {
observed_at,
duration,
} = self.remote_state.backoff
{
let elapsed_time = observed_at.elapsed();
if elapsed_time >= duration {
self.remote_state.backoff = BackoffState::Ok;
} else {
let remaining = duration - elapsed_time;
return Err(Error::BackoffError(remaining.as_secs()));
}
}
Ok(())
}
fn handle_backoff_hint(&mut self, response: &Response) -> Result<()> {
let extract_backoff_header = |header| -> Result<u64> {
Ok(response
.headers
.get_as::<u64, _>(header)
.transpose()
.unwrap_or_default() // Ignore number parsing errors.
.unwrap_or(0))
};
// In practice these two headers are mutually exclusive.
let backoff = extract_backoff_header(HEADER_BACKOFF)?;
let retry_after = extract_backoff_header(HEADER_RETRY_AFTER)?;
let max_backoff = backoff.max(retry_after);
if max_backoff > 0 {
self.remote_state.backoff = BackoffState::Backoff {
observed_at: Instant::now(),
duration: Duration::from_secs(max_backoff),
};
}
Ok(())
}
}
impl ApiClient for ViaductApiClient {
@@ -803,14 +775,14 @@ impl Client {
fn make_request(&self, url: Url) -> Result<Response> {
let mut current_remote_state = self.remote_state.lock();
self.ensure_no_backoff(&mut current_remote_state.backoff)?;
current_remote_state.ensure_no_backoff()?;
drop(current_remote_state);
let req = Request::get(url);
let resp = req.send()?;
let mut current_remote_state = self.remote_state.lock();
self.handle_backoff_hint(&resp, &mut current_remote_state.backoff)?;
current_remote_state.handle_backoff_hint(&resp)?;
if resp.is_success() {
Ok(resp)
@@ -821,50 +793,6 @@ impl Client {
)))
}
}
fn ensure_no_backoff(&self, current_state: &mut BackoffState) -> Result<()> {
if let BackoffState::Backoff {
observed_at,
duration,
} = *current_state
{
let elapsed_time = observed_at.elapsed();
if elapsed_time >= duration {
*current_state = BackoffState::Ok;
} else {
let remaining = duration - elapsed_time;
return Err(Error::BackoffError(remaining.as_secs()));
}
}
Ok(())
}
fn handle_backoff_hint(
&self,
response: &Response,
current_state: &mut BackoffState,
) -> Result<()> {
let extract_backoff_header = |header| -> Result<u64> {
Ok(response
.headers
.get_as::<u64, _>(header)
.transpose()
.unwrap_or_default() // Ignore number parsing errors.
.unwrap_or(0))
};
// In practice these two headers are mutually exclusive.
let backoff = extract_backoff_header(HEADER_BACKOFF)?;
let retry_after = extract_backoff_header(HEADER_RETRY_AFTER)?;
let max_backoff = backoff.max(retry_after);
if max_backoff > 0 {
*current_state = BackoffState::Backoff {
observed_at: Instant::now(),
duration: Duration::from_secs(max_backoff),
};
}
Ok(())
}
}
/// Stores all the endpoints for a Remote Settings server
@@ -1027,6 +955,48 @@ impl Default for RemoteState {
}
}
impl RemoteState {
pub fn handle_backoff_hint(&mut self, response: &Response) -> Result<()> {
let extract_backoff_header = |header| -> Result<u64> {
Ok(response
.headers
.get_as::<u64, _>(header)
.transpose()
.unwrap_or_default() // Ignore number parsing errors.
.unwrap_or(0))
};
// In practice these two headers are mutually exclusive.
let backoff = extract_backoff_header(HEADER_BACKOFF)?;
let retry_after = extract_backoff_header(HEADER_RETRY_AFTER)?;
let max_backoff = backoff.max(retry_after);
if max_backoff > 0 {
self.backoff = BackoffState::Backoff {
observed_at: Instant::now(),
duration: Duration::from_secs(max_backoff),
};
}
Ok(())
}
pub fn ensure_no_backoff(&mut self) -> Result<()> {
if let BackoffState::Backoff {
observed_at,
duration,
} = self.backoff
{
let elapsed_time = observed_at.elapsed();
if elapsed_time >= duration {
self.backoff = BackoffState::Ok;
} else {
let remaining = duration - elapsed_time;
return Err(Error::BackoffError(remaining.as_secs()));
}
}
Ok(())
}
}
/// Used in handling backoff responses from the Remote Settings server.
#[derive(Clone, Copy, Debug)]
pub(crate) enum BackoffState {
@@ -2040,6 +2010,76 @@ mod jexl_tests {
Some(vec![])
);
}
#[test]
fn test_update_jexl_context() {
let mut api_client = MockApiClient::new();
let records = vec![RemoteSettingsRecord {
id: "record-0001".into(),
last_modified: 100,
deleted: false,
attachment: None,
fields: serde_json::json!({
"filter_expression": "env.country == \"US\""
})
.as_object()
.unwrap()
.clone(),
}];
let changeset = ChangesetResponse {
changes: records.clone(),
timestamp: 42,
metadata: CollectionMetadata::default(),
};
api_client.expect_collection_url().returning(|| {
"http://rs.example.com/v1/buckets/main/collections/test-collection".into()
});
api_client.expect_fetch_changeset().returning({
let changeset = changeset.clone();
move |timestamp| {
assert_eq!(timestamp, None);
Ok(changeset.clone())
}
});
api_client.expect_is_prod_server().returning(|| Ok(false));
let context = RemoteSettingsContext {
country: Some("US".to_string()),
..Default::default()
};
let mut storage = Storage::new(":memory:".into());
let _ = storage.insert_collection_content(
"http://rs.example.com/v1/buckets/main/collections/test-collection",
&records,
42,
CollectionMetadata::default(),
);
let rs_client = RemoteSettingsClient::new_from_parts(
"test-collection".into(),
storage,
JexlFilter::new(Some(context)),
api_client,
);
assert_eq!(
rs_client.get_records(false).expect("Error getting records"),
Some(records)
);
// We can't call `update_config` directly, since that only works with a real API client.
// Instead, just execute the code from that method that updates the JEXL filter.
rs_client.inner.lock().jexl_filter = JexlFilter::new(Some(RemoteSettingsContext {
country: Some("UK".to_string()),
..Default::default()
}));
assert_eq!(
rs_client.get_records(false).expect("Error getting records"),
Some(vec![])
);
}
}
#[cfg(feature = "signatures")]

View File

@@ -165,4 +165,8 @@ impl BaseUrl {
// error for cannot-be-a-base URLs.
self.url.path_segments_mut().unwrap()
}
pub fn query_pairs_mut(&mut self) -> url::form_urlencoded::Serializer<'_, url::UrlQuery<'_>> {
self.url.query_pairs_mut()
}
}

View File

@@ -3,16 +3,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use std::{
collections::HashSet,
collections::{HashMap, HashSet},
sync::{Arc, Weak},
};
use camino::Utf8PathBuf;
use parking_lot::Mutex;
use serde::Deserialize;
use viaduct::Request;
use crate::{
config::BaseUrl, storage::Storage, RemoteSettingsClient, RemoteSettingsConfig2,
RemoteSettingsContext, RemoteSettingsServer, Result,
client::RemoteState, config::BaseUrl, error::Error, storage::Storage, RemoteSettingsClient,
RemoteSettingsConfig2, RemoteSettingsContext, RemoteSettingsServer, Result,
};
/// Internal Remote settings service API
@@ -25,6 +27,7 @@ struct RemoteSettingsServiceInner {
base_url: BaseUrl,
bucket_name: String,
app_context: Option<RemoteSettingsContext>,
remote_state: RemoteState,
/// Weakrefs for all clients that we've created. Note: this stores the
/// top-level/public `RemoteSettingsClient` structs rather than `client::RemoteSettingsClient`.
/// The reason for this is that we return Arcs to the public struct to the foreign code, so we
@@ -51,6 +54,7 @@ impl RemoteSettingsService {
base_url,
bucket_name,
app_context: config.app_context,
remote_state: RemoteState::default(),
clients: vec![],
}),
}
@@ -81,13 +85,30 @@ impl RemoteSettingsService {
// Make sure we only sync each collection once, even if there are multiple clients
let mut synced_collections = HashSet::new();
// TODO: poll the server using `/buckets/monitor/collections/changes/changeset` to fetch
// the current timestamp for all collections. That way we can avoid fetching collections
// we know haven't changed and also pass the `?_expected{ts}` param to the server.
let mut inner = self.inner.lock();
let changes = inner.fetch_changes()?;
let change_map: HashMap<_, _> = changes
.changes
.iter()
.map(|c| ((c.collection.as_str(), &c.bucket), c.last_modified))
.collect();
let bucket_name = inner.bucket_name.clone();
for client in self.inner.lock().active_clients() {
if synced_collections.insert(client.collection_name()) {
client.internal.sync()?;
for client in inner.active_clients() {
let client = &client.internal;
let collection_name = client.collection_name();
if let Some(client_last_modified) = client.get_last_modified_timestamp()? {
if let Some(server_last_modified) = change_map.get(&(collection_name, &bucket_name))
{
if client_last_modified != *server_last_modified {
log::trace!("skipping up-to-date collection: {collection_name}");
continue;
}
}
}
if synced_collections.insert(collection_name.to_string()) {
log::trace!("syncing collection: {collection_name}");
client.sync()?;
}
}
Ok(synced_collections.into_iter().collect())
@@ -105,12 +126,15 @@ impl RemoteSettingsService {
let bucket_name = config.bucket_name.unwrap_or_else(|| String::from("main"));
let mut inner = self.inner.lock();
for client in inner.active_clients() {
client
.internal
.update_config(base_url.clone(), bucket_name.clone())?;
client.internal.update_config(
base_url.clone(),
bucket_name.clone(),
config.app_context.clone(),
)?;
}
inner.base_url = base_url;
inner.bucket_name = bucket_name;
inner.app_context = config.app_context;
Ok(())
}
}
@@ -131,4 +155,52 @@ impl RemoteSettingsServiceInner {
});
active_clients
}
fn fetch_changes(&mut self) -> Result<Changes> {
let mut url = self.base_url.clone();
url.path_segments_mut()
.push("buckets")
.push("monitor")
.push("collections")
.push("changes")
.push("changeset");
// For now, always use `0` for the expected value. This means we'll get updates based on
// the default TTL of 1 hour.
//
// Eventually, we should add support for push notifications and use the timestamp from the
// notification.
url.query_pairs_mut().append_pair("_expected", "0");
let url = url.into_inner();
log::trace!("make_request: {url}");
self.remote_state.ensure_no_backoff()?;
let req = Request::get(url);
let resp = req.send()?;
self.remote_state.handle_backoff_hint(&resp)?;
if resp.is_success() {
Ok(resp.json()?)
} else {
Err(Error::ResponseError(format!(
"status code: {}",
resp.status
)))
}
}
}
/// Data from the changes endpoint
///
/// https://remote-settings.readthedocs.io/en/latest/client-specifications.html#endpoints
#[derive(Debug, Deserialize)]
struct Changes {
changes: Vec<ChangesCollection>,
}
#[derive(Debug, Deserialize)]
struct ChangesCollection {
collection: String,
bucket: String,
last_modified: u64,
}

View File

@@ -1 +1 @@
{"files":{"Cargo.toml":"ac0ac2103375f1c3906436b53627f88515da74864dd3f86ebb2a18952ba72b30","README.md":"d59a6ad6232a86a7bd3632ca62c44ba8bd466615c5d47ce0d836b270bac5562c","android/build.gradle":"e3b617d653aa0221f2229bb16c2fd635003fe82d0274c4b9a6f2d8154851985a","android/proguard-rules.pro":"1cf8c57e8f79c250b0af9c1a5a4edad71a5c348a79ab70243b6bae086c150ad2","android/src/main/AndroidManifest.xml":"0a05039a6124be0296764c2b0f41e863b5538d75e6164dd9ae945b59d983c318","src/configuration_overrides_types.rs":"220a5e12ee3deb309a1571c5820ec5132c959f56667c4c48f997bbe2be0c7eeb","src/configuration_types.rs":"a495199fc19cf9ce1aefe41058a38a0ffad4eb6f719fac11c11dcc3cfe4f234a","src/environment_matching.rs":"5a1ade9a900942c62e8740597528a34df6fb3fdb72c801a647a3386acd42fcc8","src/error.rs":"d3c1eda7a8da15446a321139d4d29dd9ceee99e916519690d5eb2d45ed628598","src/filter.rs":"88a39e594397708db888726cac00ad1e5b4a892c5a121c96cc11d20f41851b45","src/lib.rs":"9c83780a74048fbbc7bbba5706067b9dc5db2ae25a0cc751687d2738903723b4","src/selector.rs":"e065e05aec54e01fd106d453ece2cfee72793c8a47569cc32ac016aef3ac265b","src/sort_helpers.rs":"12d41c34fc2ca5387edc248189335fb11702618b7253f8b486f0c84576084faa","src/types.rs":"8721ccf9443b28435ba211d5705f4a6c82eb0354ba1d100045438800c7e2bf9a","uniffi.toml":"96f1cd569483ff59e3c73852f085a03889fa24a2ce20ff7a3003799a9f48a51e"},"package":null}
{"files":{"Cargo.toml":"ac0ac2103375f1c3906436b53627f88515da74864dd3f86ebb2a18952ba72b30","README.md":"d59a6ad6232a86a7bd3632ca62c44ba8bd466615c5d47ce0d836b270bac5562c","android/build.gradle":"e3b617d653aa0221f2229bb16c2fd635003fe82d0274c4b9a6f2d8154851985a","android/proguard-rules.pro":"1cf8c57e8f79c250b0af9c1a5a4edad71a5c348a79ab70243b6bae086c150ad2","android/src/main/AndroidManifest.xml":"0a05039a6124be0296764c2b0f41e863b5538d75e6164dd9ae945b59d983c318","src/configuration_overrides_types.rs":"220a5e12ee3deb309a1571c5820ec5132c959f56667c4c48f997bbe2be0c7eeb","src/configuration_types.rs":"a495199fc19cf9ce1aefe41058a38a0ffad4eb6f719fac11c11dcc3cfe4f234a","src/environment_matching.rs":"5a1ade9a900942c62e8740597528a34df6fb3fdb72c801a647a3386acd42fcc8","src/error.rs":"d3c1eda7a8da15446a321139d4d29dd9ceee99e916519690d5eb2d45ed628598","src/filter.rs":"88a39e594397708db888726cac00ad1e5b4a892c5a121c96cc11d20f41851b45","src/lib.rs":"9c83780a74048fbbc7bbba5706067b9dc5db2ae25a0cc751687d2738903723b4","src/selector.rs":"6585b4d487179353f4dc8bae396cc18a97e5fcde58f6afa09373cd04510daa03","src/sort_helpers.rs":"5bcae57c230e1d1123d3c5be1ae38b481c6a1fc5096bc0fdede6f7a7c8d27032","src/types.rs":"8721ccf9443b28435ba211d5705f4a6c82eb0354ba1d100045438800c7e2bf9a","uniffi.toml":"96f1cd569483ff59e3c73852f085a03889fa24a2ce20ff7a3003799a9f48a51e"},"package":null}

View File

@@ -1592,7 +1592,7 @@ mod tests {
"recordType": "engine",
"identifier": "b-engine",
"base": {
"name": "b-engine",
"name": "First Alphabetical",
"classification": "general",
"urls": {
"search": {
@@ -1611,7 +1611,7 @@ mod tests {
"recordType": "engine",
"identifier": "a-engine",
"base": {
"name": "a-engine",
"name": "Last Alphabetical",
"classification": "general",
"urls": {
"search": {
@@ -1731,10 +1731,10 @@ mod tests {
"default-engine".to_string(),
"default-private-engine".to_string(),
"after-defaults".to_string(),
"a-engine".to_string(),
"b-engine".to_string(),
"a-engine".to_string(),
],
"Should order the default engine first, default private engine second, and the rest of the engines based on order hint then alphabetically."
"Should order the default engine first, default private engine second, and the rest of the engines based on order hint then alphabetically by name."
);
let starts_with_wiki_config = Arc::clone(&selector).set_search_config(
@@ -1919,6 +1919,18 @@ mod tests {
selector
}
fn mock_changes_endpoint() -> mockito::Mock {
mock(
"GET",
"/v1/buckets/monitor/collections/changes/changeset?_expected=0",
)
.with_body(response_body_changes())
.with_status(200)
.with_header("content-type", "application/json")
.with_header("etag", "\"1000\"")
.create()
}
fn response_body() -> String {
json!({
"metadata": {
@@ -2021,6 +2033,20 @@ mod tests {
.to_string()
}
fn response_body_changes() -> String {
json!({
"timestamp": 1000,
"changes": [
{
"collection": "search-config-v2",
"bucket": "main",
"last_modified": 1000,
}
],
})
.to_string()
}
fn response_body_locales() -> String {
json!({
"metadata": {
@@ -2129,6 +2155,7 @@ mod tests {
#[test]
fn test_remote_settings_empty_search_config_records_throws_error() {
let changes_mock = mock_changes_endpoint();
let m = mock(
"GET",
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2168,11 +2195,13 @@ mod tests {
.unwrap_err()
.to_string()
.contains("No search config v2 records received from remote settings"));
changes_mock.expect(1).assert();
m.expect(1).assert();
}
#[test]
fn test_remote_settings_search_config_records_is_none_throws_error() {
let changes_mock = mock_changes_endpoint();
let m1 = mock(
"GET",
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2197,11 +2226,13 @@ mod tests {
.unwrap_err()
.to_string()
.contains("No search config v2 records received from remote settings"));
changes_mock.expect(1).assert();
m1.expect(1).assert();
}
#[test]
fn test_remote_settings_empty_search_config_overrides_filtered_without_error() {
let changes_mock = mock_changes_endpoint();
let m1 = mock(
"GET",
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2248,12 +2279,14 @@ mod tests {
"Should have filtered the configuration using an empty search config overrides without causing an error. {:?}",
result
);
changes_mock.expect(1).assert();
m1.expect(1).assert();
m2.expect(1).assert();
}
#[test]
fn test_remote_settings_search_config_overrides_records_is_none_throws_error() {
let changes_mock = mock_changes_endpoint();
let m1 = mock(
"GET",
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2288,12 +2321,14 @@ mod tests {
.unwrap_err()
.to_string()
.contains("No search config overrides v2 records received from remote settings"));
changes_mock.expect(1).assert();
m1.expect(1).assert();
m2.expect(1).assert();
}
#[test]
fn test_filter_with_remote_settings_overrides() {
let changes_mock = mock_changes_endpoint();
let m1 = mock(
"GET",
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2355,12 +2390,15 @@ mod tests {
test_engine.clone(),
"Should have applied the overrides to the matching engine"
);
changes_mock.expect(1).assert();
m1.expect(1).assert();
m2.expect(1).assert();
}
#[test]
fn test_filter_with_remote_settings() {
let changes_mock = mock_changes_endpoint();
let m = mock(
"GET",
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2468,11 +2506,13 @@ mod tests {
},
"Should have selected the private default engine for the matching specific default"
);
changes_mock.expect(1).assert();
m.expect(1).assert();
}
#[test]
fn test_filter_with_remote_settings_negotiate_locales() {
let changes_mock = mock_changes_endpoint();
let m = mock(
"GET",
"/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
@@ -2551,6 +2591,7 @@ mod tests {
},
"Should have selected the en-us engine when given another english locale we don't support"
);
changes_mock.expect(1).assert();
m.expect(1).assert();
}

View File

@@ -32,7 +32,7 @@ pub(crate) fn sort(
// See Bug 1945295: https://bugzilla.mozilla.org/show_bug.cgi?id=1945295
// If order is equal and order_hint is None for both, fall back to alphabetical sorting
if order == std::cmp::Ordering::Equal {
return a.identifier.cmp(&b.identifier);
return a.name.cmp(&b.name);
}
order
@@ -76,9 +76,14 @@ mod tests {
use crate::types::*;
use pretty_assertions::assert_eq;
fn create_engine(engine_id: &str, order_hint: Option<u32>) -> SearchEngineDefinition {
fn create_engine(
engine_id: &str,
order_hint: Option<u32>,
name: Option<&str>,
) -> SearchEngineDefinition {
SearchEngineDefinition {
identifier: engine_id.to_string(),
name: name.unwrap_or(engine_id).to_string(),
order_hint,
..Default::default()
}
@@ -87,9 +92,9 @@ mod tests {
#[test]
fn test_find_engine_with_match_mut_starts_with() {
let mut engines = vec![
create_engine("wiki-ca", None),
create_engine("wiki-uk", None),
create_engine("test-engine", None),
create_engine("wiki-ca", None, None),
create_engine("wiki-uk", None, None),
create_engine("test-engine", None, None),
];
let found_engine = find_engine_with_match_mut(&mut engines, &"wiki*".to_string());
@@ -103,9 +108,9 @@ mod tests {
#[test]
fn test_set_engine_order_full_list() {
let mut engines = vec![
create_engine("last-engine", None),
create_engine("secondary-engine", None),
create_engine("primary-engine", None),
create_engine("last-engine", None, None),
create_engine("secondary-engine", None, None),
create_engine("primary-engine", None, None),
];
let ordered_engines_list = vec![
"primary-engine".to_string(),
@@ -133,9 +138,9 @@ mod tests {
#[test]
fn test_set_engine_order_partial_list() {
let mut engines = vec![
create_engine("secondary-engine", None),
create_engine("primary-engine", None),
create_engine("no-order-hint-engine", None),
create_engine("secondary-engine", None, None),
create_engine("primary-engine", None, None),
create_engine("no-order-hint-engine", None, None),
];
let ordered_engines_list =
vec!["primary-engine".to_string(), "secondary-engine".to_string()];
@@ -161,9 +166,9 @@ mod tests {
let default_engine_id = None;
let default_private_engine_id = None;
let mut engines = vec![
create_engine("c-engine", Some(3)),
create_engine("b-engine", Some(2)),
create_engine("a-engine", Some(1)),
create_engine("c-engine", Some(3), None),
create_engine("b-engine", Some(2), None),
create_engine("a-engine", Some(1), None),
];
engines.sort_by(|a, b| {
sort(
@@ -187,9 +192,9 @@ mod tests {
let default_engine_id = None;
let default_private_engine_id = None;
let mut engines = vec![
create_engine("c-engine", None),
create_engine("b-engine", None),
create_engine("a-engine", None),
create_engine("c-engine", None, None),
create_engine("b-engine", None, None),
create_engine("a-engine", None, None),
];
engines.sort_by(|a, b| {
sort(
@@ -213,12 +218,14 @@ mod tests {
let default_engine_id = None;
let default_private_engine_id = None;
let mut engines = vec![
create_engine("f-engine", None),
create_engine("e-engine", None),
create_engine("d-engine", None),
create_engine("c-engine", Some(4)),
create_engine("b-engine", Some(5)),
create_engine("a-engine", Some(6)),
// Identifiers are the opposite order to the names, so that we
// can show that we are sorting alphabetically by name.
create_engine("d-engine", None, Some("Charlie")),
create_engine("e-engine", None, Some("Beta")),
create_engine("f-engine", None, Some("Alpha")),
create_engine("c-engine", Some(4), None),
create_engine("b-engine", Some(5), None),
create_engine("a-engine", Some(6), None),
];
engines.sort_by(|a, b| {
sort(
@@ -231,7 +238,7 @@ mod tests {
let actual_order: Vec<&str> = engines.iter().map(|e| e.identifier.as_str()).collect();
let expected_order = vec![
"a-engine", "b-engine", "c-engine", "d-engine", "e-engine", "f-engine",
"a-engine", "b-engine", "c-engine", "f-engine", "e-engine", "d-engine",
];
assert_eq!(
actual_order, expected_order,
@@ -244,9 +251,9 @@ mod tests {
let default_engine_id = Some("a-engine".to_string());
let default_private_engine_id = Some("b-engine".to_string());
let mut engines = vec![
create_engine("c-engine", Some(3)),
create_engine("a-engine", Some(1)), // Default engine should be first
create_engine("b-engine", Some(2)), // Default private engine should be second
create_engine("c-engine", Some(3), None),
create_engine("a-engine", Some(1), None), // Default engine should be first
create_engine("b-engine", Some(2), None), // Default private engine should be second
];
engines.sort_by(|a, b| {
sort(

View File

@@ -1 +1 @@
{"files":{"Cargo.toml":"922b2e4d85f325dbef99d5e439558a75dbfda82e1d263c2dd3bfadac9879df18","README.md":"5e28baf874b643d756228bdab345e287bf107d3182dfe6a18aafadcc4b9a3fc9","benches/benchmark_all.rs":"5909dfb1e62793afb1f2bc15b75914527a4d14fce6796307c04a309e45c0598c","metrics.yaml":"0540ab2271aeab7f07335c7ceec12acde942995f9dcb3c29070489aa61899d56","src/benchmarks/README.md":"ccee8dbddba8762d0453fa855bd6984137b224b8c019f3dd8e86a3c303f51d71","src/benchmarks/client.rs":"e5897d4e2eda06809fa6dc6db4e780b9ef266f613fb113aa6613b83f7005dd0b","src/benchmarks/geoname.rs":"00fab05cf9465cf8e22e143cde75a81885411001b240af00efda4071975d0563","src/benchmarks/ingest.rs":"1f3b5eca704c51bc8f972e7a3492a518516461e5834f97a5f7d1855a048ab16b","src/benchmarks/mod.rs":"2c9a39b7a5144674d2475f4d7d69d77c4545f9aa5f123968cb32574e76f10b1a","src/benchmarks/query.rs":"d54946063e72cf98e7f46d94665c17c66af637774c2bb50cd5798dbe63d74f3c","src/bin/debug_ingestion_sizes.rs":"ce6e810be7b3fc19e826d75b622b82cfab5a1a99397a6d0833c2c4eebff2d364","src/config.rs":"0ca876e845841bb6429862c0904c82265003f53b55aea053fac60aed278586a7","src/db.rs":"c22aab621ae8c1b70595c2073e62ff766272400be13f393327c27451bce10498","src/error.rs":"e2ef3ec0e0b2b8ecbb8f2f1717d4cb753af06913b8395d086b7643098ad100a7","src/fakespot.rs":"f501c9fe5296e7c130a9fcb532b861465717652cb5ef688230bc7a3b94df91b1","src/geoname.rs":"77376dbc7d06532a7797a93b863f150317df7f31d9200d375c8ea489ac8bee6f","src/lib.rs":"a4c0989a01a7c13184049c1f11bc7813cd3cbfb6354fcca1f5a7204e45a0dc9c","src/metrics.rs":"871f0d834efbbc9e26d61f66fa31f0021dcf41444746cd7c082f93ba9628e399","src/pocket.rs":"1316668840ec9b4ea886223921dc9d3b5a1731d1a5206c0b1089f2a6c45c1b7b","src/provider.rs":"e85d606e98a8ba37557072f91c6906b1a4d7c5586a9913bf3570ef25106b007f","src/query.rs":"66f229272c9245eb8ee0cab237071627aec599f145f64da8894bcaeb1ed7c6f9","src/rs.rs":"ad2edb0301c0510fb31b12c2f2eb65c5aee596caaee39ed5f98c1de3149afcdf","src/schema.rs":"4fe542fc6572780679854e238ee6b03baa72ca810767c935b435f79d096d3b9b","src/store.rs":"167fc224c1f56a3e3f29ec6a74091b060556993a2e010e6099b095e89df41d57","src/suggestion.rs":"bd4acd1d7f0bfd4ceebd52d44d4ea6ac639d32ef43819e217fbffaa9346b50f2","src/testing/client.rs":"47a32fd84c733001f11e8bfff94dc8c060b6b0780346dca5ddc7a5f5489c1d85","src/testing/data.rs":"6b3dad0414dd862d939f31672547e33a852056e8f891cfec9c1a9cc9fb91d54d","src/testing/mod.rs":"34120abb160a913069c3f7df03c4f52815be3461dbbf08fb0fcc4251a517ba71","src/util.rs":"52c6ec405637afa2d1a89f29fbbb7dcc341546b6deb97d326c4490bbf8713cb0","src/weather.rs":"7cc9167dcdfca49d6ad91eba6fba4d5fd49f45052f25a7fe3ad6749d3e6783fb","src/yelp.rs":"f147608a27030a0e7821d3fd050dc54a41305dc29431970733cafd59e4e5ace6","uniffi.toml":"8205e4679ac26d53e70af0f85c013fd27cda1119f4322aebf5f2b9403d45a611"},"package":null}
{"files":{"Cargo.toml":"922b2e4d85f325dbef99d5e439558a75dbfda82e1d263c2dd3bfadac9879df18","README.md":"5e28baf874b643d756228bdab345e287bf107d3182dfe6a18aafadcc4b9a3fc9","benches/benchmark_all.rs":"5909dfb1e62793afb1f2bc15b75914527a4d14fce6796307c04a309e45c0598c","metrics.yaml":"0540ab2271aeab7f07335c7ceec12acde942995f9dcb3c29070489aa61899d56","src/benchmarks/README.md":"ccee8dbddba8762d0453fa855bd6984137b224b8c019f3dd8e86a3c303f51d71","src/benchmarks/client.rs":"e5897d4e2eda06809fa6dc6db4e780b9ef266f613fb113aa6613b83f7005dd0b","src/benchmarks/geoname.rs":"00fab05cf9465cf8e22e143cde75a81885411001b240af00efda4071975d0563","src/benchmarks/ingest.rs":"1f3b5eca704c51bc8f972e7a3492a518516461e5834f97a5f7d1855a048ab16b","src/benchmarks/mod.rs":"2c9a39b7a5144674d2475f4d7d69d77c4545f9aa5f123968cb32574e76f10b1a","src/benchmarks/query.rs":"d54946063e72cf98e7f46d94665c17c66af637774c2bb50cd5798dbe63d74f3c","src/bin/debug_ingestion_sizes.rs":"ce6e810be7b3fc19e826d75b622b82cfab5a1a99397a6d0833c2c4eebff2d364","src/config.rs":"0ca876e845841bb6429862c0904c82265003f53b55aea053fac60aed278586a7","src/db.rs":"c22aab621ae8c1b70595c2073e62ff766272400be13f393327c27451bce10498","src/error.rs":"e2ef3ec0e0b2b8ecbb8f2f1717d4cb753af06913b8395d086b7643098ad100a7","src/fakespot.rs":"f501c9fe5296e7c130a9fcb532b861465717652cb5ef688230bc7a3b94df91b1","src/geoname.rs":"77376dbc7d06532a7797a93b863f150317df7f31d9200d375c8ea489ac8bee6f","src/lib.rs":"a4c0989a01a7c13184049c1f11bc7813cd3cbfb6354fcca1f5a7204e45a0dc9c","src/metrics.rs":"871f0d834efbbc9e26d61f66fa31f0021dcf41444746cd7c082f93ba9628e399","src/pocket.rs":"1316668840ec9b4ea886223921dc9d3b5a1731d1a5206c0b1089f2a6c45c1b7b","src/provider.rs":"e85d606e98a8ba37557072f91c6906b1a4d7c5586a9913bf3570ef25106b007f","src/query.rs":"66f229272c9245eb8ee0cab237071627aec599f145f64da8894bcaeb1ed7c6f9","src/rs.rs":"3e2310d069b4cbc7447c2bb625f03bb49439b218a1e8f04190015a31cde22842","src/schema.rs":"68dbdc960097cc3421247cd9f705f6dcf74c9d357b37a5824b80e37837cbf053","src/store.rs":"76e6e2134d1d0e6f8dcf30ed65fe18eb093531bdddec461ad708b1eb4ac6a01c","src/suggestion.rs":"33dd2fb8e966a72f9843476bc006c8dfb1326ed1268ad88aa91801356f2623a1","src/testing/client.rs":"47a32fd84c733001f11e8bfff94dc8c060b6b0780346dca5ddc7a5f5489c1d85","src/testing/data.rs":"ad710b31532a9540491a73cba33a54db02e85dd5ec0a4f2260430f144c3d7380","src/testing/mod.rs":"fe930be25229517831111fb6d7796ae957ec0eb1b9a190c59cf538ac41ae27f5","src/util.rs":"52c6ec405637afa2d1a89f29fbbb7dcc341546b6deb97d326c4490bbf8713cb0","src/weather.rs":"7cc9167dcdfca49d6ad91eba6fba4d5fd49f45052f25a7fe3ad6749d3e6783fb","src/yelp.rs":"1fe3b7eb6b3f7462e9758b6eb62457dfa26f7549a8290cdff7637d2fb3ffea4f","uniffi.toml":"8205e4679ac26d53e70af0f85c013fd27cda1119f4322aebf5f2b9403d45a611"},"package":null}

View File

@@ -474,6 +474,8 @@ impl ToSql for DownloadedYelpLocationSign {
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedYelpSuggestion {
pub subjects: Vec<String>,
#[serde(rename = "businessSubjects")]
pub business_subjects: Option<Vec<String>>,
#[serde(rename = "preModifiers")]
pub pre_modifiers: Vec<String>,
#[serde(rename = "postModifiers")]

View File

@@ -23,7 +23,7 @@ use sql_support::{
/// `clear_database()` by adding their names to `conditional_tables`, unless
/// they are cleared via a deletion trigger or there's some other good
/// reason not to do so.
pub const VERSION: u32 = 37;
pub const VERSION: u32 = 38;
/// The current Suggest database schema.
pub const SQL: &str = "
@@ -161,6 +161,7 @@ CREATE TABLE icons(
CREATE TABLE yelp_subjects(
keyword TEXT PRIMARY KEY,
subject_type INTEGER NOT NULL DEFAULT 0,
record_id TEXT NOT NULL
) WITHOUT ROWID;
@@ -647,6 +648,20 @@ impl ConnectionInitializer for SuggestConnectionInitializer<'_> {
tx.execute_batch("DROP TABLE IF EXISTS yelp_location_signs;")?;
Ok(())
}
37 => {
clear_database(tx)?;
tx.execute_batch(
"
DROP TABLE yelp_subjects;
CREATE TABLE yelp_subjects(
keyword TEXT PRIMARY KEY,
subject_type INTEGER NOT NULL DEFAULT 0,
record_id TEXT NOT NULL
) WITHOUT ROWID;
",
)?;
Ok(())
}
_ => Err(open_database::Error::IncompatibleVersion(version)),
}
}

View File

@@ -975,6 +975,7 @@ impl SuggestStoreDbs {
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::suggestion::YelpSubjectType;
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -2247,6 +2248,25 @@ pub(crate) mod tests {
)
.has_location_sign(false)],
);
// Business subject.
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("the shop tokyo")),
vec![ramen_suggestion(
"the shop tokyo",
"https://www.yelp.com/search?find_desc=the+shop&find_loc=tokyo"
)
.has_location_sign(false)
.subject_type(YelpSubjectType::Business)]
);
assert_eq!(
store.fetch_suggestions(SuggestionQuery::yelp("the sho")),
vec![
ramen_suggestion("the shop", "https://www.yelp.com/search?find_desc=the+shop")
.has_location_sign(false)
.subject_exact_match(false)
.subject_type(YelpSubjectType::Business)
]
);
Ok(())
}

View File

@@ -16,6 +16,16 @@ const TIMESTAMP_TEMPLATE: &str = "%YYYYMMDDHH%";
/// 2 bytes shorter than [`TIMESTAMP_TEMPLATE`].
const TIMESTAMP_LENGTH: usize = 10;
/// Subject type for Yelp suggestion.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, uniffi::Enum)]
#[repr(u8)]
pub enum YelpSubjectType {
// Service such as sushi, ramen, yoga etc.
Service = 0,
// Specific business such as the shop name.
Business = 1,
}
/// A suggestion from the database to show in the address bar.
#[derive(Clone, Debug, PartialEq, uniffi::Enum)]
pub enum Suggestion {
@@ -66,6 +76,7 @@ pub enum Suggestion {
score: f64,
has_location_sign: bool,
subject_exact_match: bool,
subject_type: YelpSubjectType,
location_param: String,
},
Mdn {

View File

@@ -4,7 +4,7 @@
//! Test data that we use in many tests
use crate::{suggestion::FtsMatchInfo, testing::MockIcon, Suggestion};
use crate::{suggestion::FtsMatchInfo, suggestion::YelpSubjectType, testing::MockIcon, Suggestion};
use serde_json::json;
use serde_json::Value as JsonValue;
@@ -294,6 +294,7 @@ pub fn burnout_suggestion(is_top_pick: bool) -> Suggestion {
pub fn ramen_yelp() -> JsonValue {
json!({
"subjects": ["ramen", "spicy ramen", "spicy random ramen", "rats", "raven", "raccoon", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789Z"],
"businessSubjects": ["the shop"],
"preModifiers": ["best", "super best", "same_modifier"],
"postModifiers": ["delivery", "super delivery", "same_modifier"],
"locationSigns": [
@@ -319,6 +320,7 @@ pub fn ramen_suggestion(title: &str, url: &str) -> Suggestion {
score: 0.5,
has_location_sign: true,
subject_exact_match: true,
subject_type: YelpSubjectType::Service,
location_param: "find_loc".into(),
}
}

View File

@@ -8,7 +8,8 @@ mod data;
pub use client::{MockAttachment, MockIcon, MockRecord, MockRemoteSettingsClient};
pub use data::*;
use crate::Suggestion;
use crate::{suggestion::YelpSubjectType, Suggestion};
use parking_lot::Once;
use serde_json::Value as JsonValue;
@@ -79,6 +80,7 @@ impl Suggestion {
icon_mimetype,
score,
subject_exact_match,
subject_type,
location_param,
..
} => Self::Yelp {
@@ -88,6 +90,7 @@ impl Suggestion {
icon_mimetype,
score,
subject_exact_match,
subject_type,
location_param,
has_location_sign,
},
@@ -103,6 +106,7 @@ impl Suggestion {
icon,
icon_mimetype,
score,
subject_type,
has_location_sign,
location_param,
..
@@ -113,10 +117,38 @@ impl Suggestion {
icon_mimetype,
score,
subject_exact_match,
subject_type,
location_param,
has_location_sign,
},
_ => panic!("has_location_sign only valid for yelp suggestions"),
_ => panic!("subject_exact_match only valid for yelp suggestions"),
}
}
pub fn subject_type(self, subject_type: YelpSubjectType) -> Self {
match self {
Self::Yelp {
title,
url,
icon,
icon_mimetype,
score,
subject_exact_match,
has_location_sign,
location_param,
..
} => Self::Yelp {
title,
url,
icon,
icon_mimetype,
score,
subject_exact_match,
subject_type,
location_param,
has_location_sign,
},
_ => panic!("subject_type only valid for yelp suggestions"),
}
}
}

View File

@@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
use rusqlite::types::ToSqlOutput;
use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef};
use rusqlite::{named_params, Result as RusqliteResult, ToSql};
use sql_support::ConnExt;
use url::form_urlencoded;
@@ -13,6 +13,7 @@ use crate::{
provider::SuggestionProvider,
rs::{DownloadedYelpSuggestion, SuggestRecordId},
suggestion::Suggestion,
suggestion::YelpSubjectType,
Result, SuggestionQuery,
};
@@ -31,6 +32,22 @@ impl ToSql for Modifier {
}
}
impl ToSql for YelpSubjectType {
fn to_sql(&self) -> RusqliteResult<ToSqlOutput<'_>> {
Ok(ToSqlOutput::from(*self as u8))
}
}
impl FromSql for YelpSubjectType {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
if value.as_i64().unwrap_or_default() == 0 {
Ok(YelpSubjectType::Service)
} else {
Ok(YelpSubjectType::Business)
}
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum FindFrom {
First,
@@ -57,6 +74,18 @@ const MAX_MODIFIER_WORDS_NUMBER: usize = 2;
/// At least this many characters must be typed for a subject to be matched.
const SUBJECT_PREFIX_MATCH_THRESHOLD: usize = 2;
#[derive(Debug, PartialEq)]
struct FindSubjectData<'a> {
// The keyword in DB (but the case is inherited by query).
subject: String,
// Whether or not the keyword is exact match.
exact_match: bool,
// The subject type.
subject_type: YelpSubjectType,
// Words after removed matching subject.
rest: &'a [&'a str],
}
impl SuggestDao<'_> {
/// Inserts the suggestions for Yelp attachment into the database.
pub(crate) fn insert_yelp_suggestions(
@@ -67,10 +96,23 @@ impl SuggestDao<'_> {
for keyword in &suggestion.subjects {
self.scope.err_if_interrupted()?;
self.conn.execute_cached(
"INSERT INTO yelp_subjects(record_id, keyword) VALUES(:record_id, :keyword)",
"INSERT INTO yelp_subjects(record_id, keyword, subject_type) VALUES(:record_id, :keyword, :subject_type)",
named_params! {
":record_id": record_id.as_str(),
":keyword": keyword,
":subject_type": YelpSubjectType::Service,
},
)?;
}
for keyword in suggestion.business_subjects.as_ref().unwrap_or(&vec![]) {
self.scope.err_if_interrupted()?;
self.conn.execute_cached(
"INSERT INTO yelp_subjects(record_id, keyword, subject_type) VALUES(:record_id, :keyword, :subject_type)",
named_params! {
":record_id": record_id.as_str(),
":keyword": keyword,
":subject_type": YelpSubjectType::Business,
},
)?;
}
@@ -163,10 +205,10 @@ impl SuggestDao<'_> {
query_words = rest;
}
let Some(subject_tuple) = self.find_subject(query_words)? else {
let Some(subject_data) = self.find_subject(query_words)? else {
return Ok(vec![]);
};
query_words = subject_tuple.2;
query_words = subject_data.rest;
let post_modifier_tuple =
self.find_modifier(query_words, Modifier::Post, FindFrom::First)?;
@@ -194,8 +236,9 @@ impl SuggestDao<'_> {
let (icon, icon_mimetype, score) = self.fetch_custom_details()?;
let builder = SuggestionBuilder {
subject: &subject_tuple.0,
subject_exact_match: subject_tuple.1,
subject: &subject_data.subject,
subject_exact_match: subject_data.exact_match,
subject_type: subject_data.subject_type,
pre_modifier: pre_modifier_tuple.map(|(words, _)| words.to_string()),
post_modifier: post_modifier_tuple.map(|(words, _)| words.to_string()),
location_sign: location_sign_tuple.map(|(words, _)| words.to_string()),
@@ -266,16 +309,8 @@ impl SuggestDao<'_> {
}
/// Find the subject for given query.
/// It returns Option<tuple> as follows:
/// (
/// String: The keyword in DB (but the case is inherited by query).
/// bool: Whether or not the keyword is exact match.
/// &[&str]: Words after removed matching subject.
/// )
fn find_subject<'a>(
&self,
query_words: &'a [&'a str],
) -> Result<Option<(String, bool, &'a [&'a str])>> {
/// It returns Option<FindSubjectData>.
fn find_subject<'a>(&self, query_words: &'a [&'a str]) -> Result<Option<FindSubjectData<'a>>> {
if query_words.is_empty() {
return Ok(None);
}
@@ -283,8 +318,8 @@ impl SuggestDao<'_> {
let mut query_string = query_words.join(" ");
// This checks if keyword is a substring of the query.
if let Some(keyword_lowercase) = self.conn.try_query_one::<String, _>(
"SELECT keyword
if let Ok((keyword_lowercase, subject_type)) = self.conn.query_row_and_then_cachable(
"SELECT keyword, subject_type
FROM yelp_subjects
WHERE :query BETWEEN keyword AND keyword || ' ' || x'FFFF'
ORDER BY LENGTH(keyword) ASC, keyword ASC
@@ -292,16 +327,20 @@ impl SuggestDao<'_> {
named_params! {
":query": query_string.to_lowercase(),
},
|row| -> Result<_> {
Ok((row.get::<_, String>(0)?, row.get::<_, YelpSubjectType>(1)?))
},
true,
)? {
) {
// Preserve the query as the user typed it including its case.
return Ok(query_string.get(0..keyword_lowercase.len()).map(|keyword| {
let count = keyword.split_whitespace().count();
(
keyword.to_string(),
true,
query_words.get(count..).unwrap_or_default(),
)
FindSubjectData {
subject: keyword.to_string(),
exact_match: true,
subject_type,
rest: query_words.get(count..).unwrap_or_default(),
}
}));
};
@@ -310,8 +349,8 @@ impl SuggestDao<'_> {
}
// Oppositely, this checks if the query is a substring of keyword.
if let Some(keyword_lowercase) = self.conn.try_query_one::<String, _>(
"SELECT keyword
if let Ok((keyword_lowercase, subject_type)) = self.conn.query_row_and_then_cachable(
"SELECT keyword, subject_type
FROM yelp_subjects
WHERE keyword BETWEEN :query AND :query || x'FFFF'
ORDER BY LENGTH(keyword) ASC, keyword ASC
@@ -319,8 +358,11 @@ impl SuggestDao<'_> {
named_params! {
":query": query_string.to_lowercase(),
},
|row| -> Result<_> {
Ok((row.get::<_, String>(0)?, row.get::<_, YelpSubjectType>(1)?))
},
true,
)? {
) {
// Preserve the query as the user typed it including its case.
return Ok(keyword_lowercase
.get(query_string.len()..)
@@ -328,11 +370,12 @@ impl SuggestDao<'_> {
query_string.push_str(keyword_rest);
let count =
std::cmp::min(query_words.len(), query_string.split_whitespace().count());
(
query_string,
false,
query_words.get(count..).unwrap_or_default(),
)
FindSubjectData {
subject: query_string,
exact_match: false,
subject_type,
rest: query_words.get(count..).unwrap_or_default(),
}
}));
};
@@ -383,6 +426,7 @@ impl SuggestDao<'_> {
struct SuggestionBuilder<'a> {
subject: &'a str,
subject_exact_match: bool,
subject_type: YelpSubjectType,
pre_modifier: Option<String>,
post_modifier: Option<String>,
location_sign: Option<String>,
@@ -435,6 +479,7 @@ impl<'a> From<SuggestionBuilder<'a>> for Suggestion {
score: builder.score,
has_location_sign: builder.location_sign.is_some(),
subject_exact_match: builder.subject_exact_match,
subject_type: builder.subject_type,
location_param: "find_loc".to_string(),
}
}
@@ -645,31 +690,114 @@ mod tests {
);
}
type FindSubjectTestCase<'a> = (&'a str, Option<(String, bool, &'a [&'a str])>);
type FindSubjectTestCase<'a> = (&'a str, Option<FindSubjectData<'a>>);
let find_subject_tests: &[FindSubjectTestCase] = &[
// Query, Expected result.
("", None),
("r", None),
("ra", Some(("rats".to_string(), false, &[]))),
("ram", Some(("ramen".to_string(), false, &[]))),
("rame", Some(("ramen".to_string(), false, &[]))),
("ramen", Some(("ramen".to_string(), true, &[]))),
("spi", Some(("spicy ramen".to_string(), false, &[]))),
("spicy ra ", Some(("spicy ramen".to_string(), false, &[]))),
("spicy ramen", Some(("spicy ramen".to_string(), true, &[]))),
(
"ra",
Some(FindSubjectData {
subject: "rats".to_string(),
exact_match: false,
subject_type: YelpSubjectType::Service,
rest: &[],
}),
),
(
"ram",
Some(FindSubjectData {
subject: "ramen".to_string(),
exact_match: false,
subject_type: YelpSubjectType::Service,
rest: &[],
}),
),
(
"rame",
Some(FindSubjectData {
subject: "ramen".to_string(),
exact_match: false,
subject_type: YelpSubjectType::Service,
rest: &[],
}),
),
(
"ramen",
Some(FindSubjectData {
subject: "ramen".to_string(),
exact_match: true,
subject_type: YelpSubjectType::Service,
rest: &[],
}),
),
(
"spi",
Some(FindSubjectData {
subject: "spicy ramen".to_string(),
exact_match: false,
subject_type: YelpSubjectType::Service,
rest: &[],
}),
),
(
"spicy ra ",
Some(FindSubjectData {
subject: "spicy ramen".to_string(),
exact_match: false,
subject_type: YelpSubjectType::Service,
rest: &[],
}),
),
(
"spicy ramen",
Some(FindSubjectData {
subject: "spicy ramen".to_string(),
exact_match: true,
subject_type: YelpSubjectType::Service,
rest: &[],
}),
),
(
"spicy ramen gogo",
Some(("spicy ramen".to_string(), true, &["gogo"])),
Some(FindSubjectData {
subject: "spicy ramen".to_string(),
exact_match: true,
subject_type: YelpSubjectType::Service,
rest: &["gogo"],
}),
),
(
"SpIcY rAmEn GoGo",
Some(("SpIcY rAmEn".to_string(), true, &["GoGo"])),
Some(FindSubjectData {
subject: "SpIcY rAmEn".to_string(),
exact_match: true,
subject_type: YelpSubjectType::Service,
rest: &["GoGo"],
}),
),
("ramenabc", None),
("ramenabc xyz", None),
("spicy ramenabc", None),
("spicy ramenabc xyz", None),
("ramen abc", Some(("ramen".to_string(), true, &["abc"]))),
(
"ramen abc",
Some(FindSubjectData {
subject: "ramen".to_string(),
exact_match: true,
subject_type: YelpSubjectType::Service,
rest: &["abc"],
}),
),
(
"the shop",
Some(FindSubjectData {
subject: "the shop".to_string(),
exact_match: true,
subject_type: YelpSubjectType::Business,
rest: &[],
}),
),
];
for (query, expected) in find_subject_tests {
assert_eq!(

View File

@@ -2945,6 +2945,7 @@ Suggestion.Yelp = class extends Suggestion{
score,
hasLocationSign,
subjectExactMatch,
subjectType,
locationParam
) {
super();
@@ -2955,6 +2956,7 @@ Suggestion.Yelp = class extends Suggestion{
this.score = score;
this.hasLocationSign = hasLocationSign;
this.subjectExactMatch = subjectExactMatch;
this.subjectType = subjectType;
this.locationParam = locationParam;
}
}
@@ -3100,6 +3102,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer {
FfiConverterF64.read(dataStream),
FfiConverterBool.read(dataStream),
FfiConverterBool.read(dataStream),
FfiConverterTypeYelpSubjectType.read(dataStream),
FfiConverterString.read(dataStream)
);
case 6:
@@ -3200,6 +3203,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer {
FfiConverterF64.write(dataStream, value.score);
FfiConverterBool.write(dataStream, value.hasLocationSign);
FfiConverterBool.write(dataStream, value.subjectExactMatch);
FfiConverterTypeYelpSubjectType.write(dataStream, value.subjectType);
FfiConverterString.write(dataStream, value.locationParam);
return;
}
@@ -3300,6 +3304,7 @@ export class FfiConverterTypeSuggestion extends FfiConverterArrayBuffer {
totalSize += FfiConverterF64.computeSize(value.score);
totalSize += FfiConverterBool.computeSize(value.hasLocationSign);
totalSize += FfiConverterBool.computeSize(value.subjectExactMatch);
totalSize += FfiConverterTypeYelpSubjectType.computeSize(value.subjectType);
totalSize += FfiConverterString.computeSize(value.locationParam);
return totalSize;
}
@@ -3477,6 +3482,63 @@ export class FfiConverterTypeSuggestionProvider extends FfiConverterArrayBuffer
}
/**
* Subject type for Yelp suggestion.
*/
export const YelpSubjectType = {
/**
* SERVICE
*/
SERVICE:0,
/**
* BUSINESS
*/
BUSINESS:1,
};
Object.freeze(YelpSubjectType);
// Export the FFIConverter object to make external types work.
export class FfiConverterTypeYelpSubjectType extends FfiConverterArrayBuffer {
static #validValues = Object.values(YelpSubjectType);
static read(dataStream) {
// Use sequential indices (1-based) for the wire format to match Python bindings
switch (dataStream.readInt32()) {
case 1:
return YelpSubjectType.SERVICE
case 2:
return YelpSubjectType.BUSINESS
default:
throw new UniFFITypeError("Unknown YelpSubjectType variant");
}
}
static write(dataStream, value) {
if (value === YelpSubjectType.SERVICE) {
dataStream.writeInt32(1);
return;
}
if (value === YelpSubjectType.BUSINESS) {
dataStream.writeInt32(2);
return;
}
throw new UniFFITypeError("Unknown YelpSubjectType variant");
}
static computeSize(value) {
return 4;
}
static checkType(value) {
// Check that the value is a valid enum variant
if (!this.#validValues.includes(value)) {
throw new UniFFITypeError(`${value} is not a valid value for YelpSubjectType`);
}
}
}
// Export the FFIConverter object to make external types work.
export class FfiConverterOptionali32 extends FfiConverterArrayBuffer {
static checkType(value) {