diff --git a/components/suggest/Cargo.toml b/components/suggest/Cargo.toml index a92056b623..b5b74fe744 100644 --- a/components/suggest/Cargo.toml +++ b/components/suggest/Cargo.toml @@ -41,6 +41,8 @@ uniffi = { workspace = true, features = ["build"] } [features] # Required for the benchmarks to work, wasted bytes otherwise. benchmark_api = ["tempfile", "viaduct-reqwest"] +# Enable fakespot suggestions. This is behind a feature flag since it's currently a WIP. +fakespot = [] [[bench]] name = "benchmark_all" diff --git a/components/suggest/build.rs b/components/suggest/build.rs index 5364bad4c1..258ba804d6 100644 --- a/components/suggest/build.rs +++ b/components/suggest/build.rs @@ -3,5 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ fn main() { + #[cfg(feature = "fakespot")] + uniffi::generate_scaffolding("./src/suggest-fakespot.udl").unwrap(); + + #[cfg(not(feature = "fakespot"))] uniffi::generate_scaffolding("./src/suggest.udl").unwrap(); } diff --git a/components/suggest/src/benchmarks/client.rs b/components/suggest/src/benchmarks/client.rs index 657600f238..d1e1bca203 100644 --- a/components/suggest/src/benchmarks/client.rs +++ b/components/suggest/src/benchmarks/client.rs @@ -4,7 +4,6 @@ use crate::{rs, Result}; use parking_lot::Mutex; -use remote_settings::{Client, RemoteSettingsConfig}; use std::collections::HashMap; /// Remotes settings client that runs during the benchmark warm-up phase. @@ -13,20 +12,14 @@ use std::collections::HashMap; /// Then it can be converted into a [RemoteSettingsBenchmarkClient], which allows benchmark code to exclude the network request time. /// [RemoteSettingsBenchmarkClient] implements [rs::Client] by getting data from a HashMap rather than hitting the network. pub struct RemoteSettingsWarmUpClient { - client: Client, + client: rs::RemoteSettingsClient, pub get_records_responses: Mutex>>, } impl RemoteSettingsWarmUpClient { pub fn new() -> Self { Self { - client: Client::new(RemoteSettingsConfig { - server: None, - server_url: None, - bucket_name: None, - collection_name: crate::rs::REMOTE_SETTINGS_COLLECTION.into(), - }) - .unwrap(), + client: rs::RemoteSettingsClient::new(None, None, None).unwrap(), get_records_responses: Mutex::new(HashMap::new()), } } @@ -40,7 +33,7 @@ impl Default for RemoteSettingsWarmUpClient { impl rs::Client for RemoteSettingsWarmUpClient { fn get_records(&self, request: rs::RecordRequest) -> Result> { - let response = ::get_records(&self.client, request.clone())?; + let response = self.client.get_records(request.clone())?; self.get_records_responses .lock() .insert(request, response.clone()); diff --git a/components/suggest/src/db.rs b/components/suggest/src/db.rs index 317d977058..6a953fd802 100644 --- a/components/suggest/src/db.rs +++ b/components/suggest/src/db.rs @@ -30,6 +30,9 @@ use crate::{ Result, SuggestionQuery, }; +#[cfg(feature = "fakespot")] +use crate::rs::DownloadedFakespotSuggestion; + /// The metadata key whose value is a JSON string encoding a /// `SuggestGlobalConfig`, which contains global Suggest configuration data. pub const GLOBAL_CONFIG_META_KEY: &str = "global_config"; @@ -216,6 +219,8 @@ impl<'a> SuggestDao<'a> { SuggestionProvider::Yelp => self.fetch_yelp_suggestions(query), SuggestionProvider::Mdn => self.fetch_mdn_suggestions(query), SuggestionProvider::Weather => self.fetch_weather_suggestions(query), + #[cfg(feature = "fakespot")] + SuggestionProvider::Fakespot => self.fetch_fakespot_suggestions(query), }?; acc.extend(suggestions); Ok(acc) @@ -672,6 +677,45 @@ impl<'a> SuggestDao<'a> { Ok(suggestions) } + #[cfg(feature = "fakespot")] + /// Fetches fakespot suggestions + pub fn fetch_fakespot_suggestions(&self, query: &SuggestionQuery) -> Result> { + // FAKESPOT-TODO: The SNG will update this based on the results of their FTS experimentation + self.conn.query_rows_and_then_cached( + r#" + SELECT + s.title, + s.url, + s.score, + f.fakespot_grade, + f.product_id, + f.rating, + f.total_reviews + FROM + suggestions s + JOIN + fakespot_custom_details f + ON f.suggestion_id = s.id + WHERE + s.title LIKE '%' || ? || '%' + ORDER BY + s.score DESC + "#, + (&query.keyword,), + |row| { + Ok(Suggestion::Fakespot { + title: row.get(0)?, + url: row.get(1)?, + score: row.get(2)?, + fakespot_grade: row.get(3)?, + product_id: row.get(4)?, + rating: row.get(5)?, + total_reviews: row.get(6)?, + }) + }, + ) + } + /// Inserts all suggestions from a downloaded AMO attachment into /// the database. pub fn insert_amo_suggestions( @@ -878,6 +922,29 @@ impl<'a> SuggestDao<'a> { Ok(()) } + /// Inserts all suggestions from a downloaded Fakespot attachment into the database. + #[cfg(feature = "fakespot")] + pub fn insert_fakespot_suggestions( + &mut self, + record_id: &SuggestRecordId, + suggestions: &[DownloadedFakespotSuggestion], + ) -> Result<()> { + // FAKESPOT-TODO: The SNG will update this based on the results of their FTS experimentation + let mut suggestion_insert = SuggestionInsertStatement::new(self.conn)?; + let mut fakespot_insert = FakespotInsertStatement::new(self.conn)?; + for suggestion in suggestions { + let suggestion_id = suggestion_insert.execute( + record_id, + &suggestion.title, + &suggestion.url, + suggestion.score, + SuggestionProvider::Fakespot, + )?; + fakespot_insert.execute(suggestion_id, suggestion)?; + } + Ok(()) + } + /// Inserts weather record data into the database. pub fn insert_weather_data( &mut self, @@ -1288,6 +1355,43 @@ impl<'conn> MdnInsertStatement<'conn> { } } +#[cfg(feature = "fakespot")] +struct FakespotInsertStatement<'conn>(rusqlite::Statement<'conn>); + +#[cfg(feature = "fakespot")] +impl<'conn> FakespotInsertStatement<'conn> { + fn new(conn: &'conn Connection) -> Result { + Ok(Self(conn.prepare( + "INSERT INTO fakespot_custom_details( + suggestion_id, + fakespot_grade, + product_id, + rating, + total_reviews + ) + VALUES(?, ?, ?, ?, ?) + ", + )?)) + } + + fn execute( + &mut self, + suggestion_id: i64, + fakespot: &DownloadedFakespotSuggestion, + ) -> Result<()> { + self.0 + .execute(( + suggestion_id, + &fakespot.fakespot_grade, + &fakespot.product_id, + fakespot.rating, + fakespot.total_reviews, + )) + .with_context("fakespot insert")?; + Ok(()) + } +} + struct KeywordInsertStatement<'conn>(rusqlite::Statement<'conn>); impl<'conn> KeywordInsertStatement<'conn> { diff --git a/components/suggest/src/lib.rs b/components/suggest/src/lib.rs index 3a7403d6e8..5ebb82a02c 100644 --- a/components/suggest/src/lib.rs +++ b/components/suggest/src/lib.rs @@ -31,4 +31,8 @@ pub use suggestion::{raw_suggestion_url_matches, Suggestion}; pub(crate) type Result = std::result::Result; pub type SuggestApiResult = std::result::Result; +#[cfg(not(feature = "fakespot"))] uniffi::include_scaffolding!("suggest"); + +#[cfg(feature = "fakespot")] +uniffi::include_scaffolding!("suggest-fakespot"); diff --git a/components/suggest/src/provider.rs b/components/suggest/src/provider.rs index 8b6c52b997..6afb4d9e46 100644 --- a/components/suggest/src/provider.rs +++ b/components/suggest/src/provider.rs @@ -22,6 +22,8 @@ pub enum SuggestionProvider { Mdn = 6, Weather = 7, AmpMobile = 8, + #[cfg(feature = "fakespot")] + Fakespot = 9, } impl FromSql for SuggestionProvider { @@ -35,6 +37,7 @@ impl FromSql for SuggestionProvider { } impl SuggestionProvider { + #[cfg(not(feature = "fakespot"))] pub fn all() -> [Self; 8] { [ Self::Amp, @@ -48,6 +51,21 @@ impl SuggestionProvider { ] } + #[cfg(feature = "fakespot")] + pub fn all() -> [Self; 9] { + [ + Self::Amp, + Self::Wikipedia, + Self::Amo, + Self::Pocket, + Self::Yelp, + Self::Mdn, + Self::Weather, + Self::AmpMobile, + Self::Fakespot, + ] + } + #[inline] pub(crate) fn from_u8(v: u8) -> Option { match v { @@ -58,6 +76,9 @@ impl SuggestionProvider { 5 => Some(SuggestionProvider::Yelp), 6 => Some(SuggestionProvider::Mdn), 7 => Some(SuggestionProvider::Weather), + 8 => Some(SuggestionProvider::AmpMobile), + #[cfg(feature = "fakespot")] + 9 => Some(SuggestionProvider::Fakespot), _ => None, } } @@ -105,6 +126,10 @@ impl SuggestionProvider { SuggestRecordType::GlobalConfig, ] } + #[cfg(feature = "fakespot")] + SuggestionProvider::Fakespot => { + vec![SuggestRecordType::Fakespot] + } } } } diff --git a/components/suggest/src/query.rs b/components/suggest/src/query.rs index 4af214a22f..5e87f168e4 100644 --- a/components/suggest/src/query.rs +++ b/components/suggest/src/query.rs @@ -95,6 +95,15 @@ impl SuggestionQuery { } } + #[cfg(feature = "fakespot")] + pub fn fakespot(keyword: &str) -> Self { + Self { + keyword: keyword.into(), + providers: vec![SuggestionProvider::Fakespot], + limit: None, + } + } + pub fn weather(keyword: &str) -> Self { Self { keyword: keyword.into(), diff --git a/components/suggest/src/rs.rs b/components/suggest/src/rs.rs index 5e1f1cbed1..40279b51a4 100644 --- a/components/suggest/src/rs.rs +++ b/components/suggest/src/rs.rs @@ -38,15 +38,13 @@ use serde::{Deserialize, Deserializer}; use crate::{error::Error, provider::SuggestionProvider, Result}; -/// The Suggest Remote Settings collection name. -pub(crate) const REMOTE_SETTINGS_COLLECTION: &str = "quicksuggest"; - /// The maximum number of suggestions in a Suggest record's attachment. /// /// This should be the same as the `BUCKET_SIZE` constant in the /// `mozilla-services/quicksuggest-rs` repo. pub(crate) const SUGGESTIONS_PER_ATTACHMENT: u64 = 200; +#[cfg(not(feature = "fakespot"))] /// A list of default record types to download if nothing is specified. /// This currently defaults to all of the record types. pub(crate) const DEFAULT_RECORDS_TYPES: [SuggestRecordType; 9] = [ @@ -61,6 +59,22 @@ pub(crate) const DEFAULT_RECORDS_TYPES: [SuggestRecordType; 9] = [ SuggestRecordType::AmpMobile, ]; +#[cfg(feature = "fakespot")] +/// A list of default record types to download if nothing is specified. +/// This currently defaults to all of the record types. +pub(crate) const DEFAULT_RECORDS_TYPES: [SuggestRecordType; 10] = [ + SuggestRecordType::Icon, + SuggestRecordType::AmpWikipedia, + SuggestRecordType::Amo, + SuggestRecordType::Pocket, + SuggestRecordType::Yelp, + SuggestRecordType::Mdn, + SuggestRecordType::Weather, + SuggestRecordType::GlobalConfig, + SuggestRecordType::AmpMobile, + SuggestRecordType::Fakespot, +]; + /// A trait for a client that downloads suggestions from Remote Settings. /// /// This trait lets tests use a mock client. @@ -69,17 +83,61 @@ pub(crate) trait Client { fn get_records(&self, request: RecordRequest) -> Result>; } -impl Client for remote_settings::Client { +/// Implements the [Client] trait using a real remote settings client +pub struct RemoteSettingsClient { + // Create a separate client for each collection name + quicksuggest_client: remote_settings::Client, + #[cfg(feature = "fakespot")] + fakespot_client: remote_settings::Client, +} + +impl RemoteSettingsClient { + pub fn new( + server: Option, + bucket_name: Option, + server_url: Option, + ) -> Result { + Ok(Self { + quicksuggest_client: remote_settings::Client::new( + remote_settings::RemoteSettingsConfig { + server: server.clone(), + bucket_name: bucket_name.clone(), + collection_name: "quicksuggest".to_owned(), + server_url: server_url.clone(), + }, + )?, + #[cfg(feature = "fakespot")] + fakespot_client: remote_settings::Client::new(remote_settings::RemoteSettingsConfig { + server, + bucket_name, + collection_name: "fakespot-suggest-products".to_owned(), + server_url, + })?, + }) + } + + fn client_for_record_type(&self, record_type: &str) -> &remote_settings::Client { + match record_type { + #[cfg(feature = "fakespot")] + "fakespot-suggestions" => &self.fakespot_client, + _ => &self.quicksuggest_client, + } + } +} + +impl Client for RemoteSettingsClient { fn get_records(&self, request: RecordRequest) -> Result> { + let client = self.client_for_record_type(request.record_type.as_str()); let options = request.into(); - self.get_records_with_options(&options)? + client + .get_records_with_options(&options)? .records .into_iter() .map(|record| { let attachment_data = record .attachment .as_ref() - .map(|a| self.get_attachment(&a.location)) + .map(|a| client.get_attachment(&a.location)) .transpose()?; Ok(Record::new(record, attachment_data)) }) @@ -89,7 +147,7 @@ impl Client for remote_settings::Client { #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] pub struct RecordRequest { - pub record_type: Option, + pub record_type: String, pub last_modified: Option, pub limit: Option, } @@ -103,9 +161,7 @@ impl From for GetItemsOptions { // so that we can eventually resume downloading where we left off. options.sort("last_modified", SortOrder::Ascending); - if let Some(record_type) = value.record_type { - options.filter_eq("type", record_type); - } + options.filter_eq("type", value.record_type); if let Some(last_modified) = value.last_modified { options.filter_gt("last_modified", last_modified.to_string()); @@ -180,6 +236,9 @@ pub(crate) enum SuggestRecord { GlobalConfig(DownloadedGlobalConfig), #[serde(rename = "amp-mobile-suggestions")] AmpMobile, + #[cfg(feature = "fakespot")] + #[serde(rename = "fakespot-suggestions")] + Fakespot, } /// Enum for the different record types that can be consumed. @@ -196,6 +255,8 @@ pub enum SuggestRecordType { Weather, GlobalConfig, AmpMobile, + #[cfg(feature = "fakespot")] + Fakespot, } impl From for SuggestRecordType { @@ -210,6 +271,8 @@ impl From for SuggestRecordType { SuggestRecord::Yelp => Self::Yelp, SuggestRecord::GlobalConfig(_) => Self::GlobalConfig, SuggestRecord::AmpMobile => Self::AmpMobile, + #[cfg(feature = "fakespot")] + SuggestRecord::Fakespot => Self::Fakespot, } } } @@ -226,6 +289,8 @@ impl fmt::Display for SuggestRecordType { Self::Weather => write!(f, "weather"), Self::GlobalConfig => write!(f, "configuration"), Self::AmpMobile => write!(f, "amp-mobile-suggestions"), + #[cfg(feature = "fakespot")] + Self::Fakespot => write!(f, "fakespot-suggestions"), } } } @@ -476,6 +541,19 @@ pub(crate) struct DownloadedMdnSuggestion { pub score: f64, } +#[cfg(feature = "fakespot")] +/// A Fakespot suggestion to ingest from an attachment +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct DownloadedFakespotSuggestion { + pub fakespot_grade: String, + pub product_id: String, + pub rating: f64, + pub score: f64, + pub title: String, + pub total_reviews: i64, + pub url: String, +} + /// Weather data to ingest from a weather record #[derive(Clone, Debug, Deserialize)] pub(crate) struct DownloadedWeatherData { diff --git a/components/suggest/src/schema.rs b/components/suggest/src/schema.rs index 1fd9ac97ef..5064fcd9d9 100644 --- a/components/suggest/src/schema.rs +++ b/components/suggest/src/schema.rs @@ -6,6 +6,7 @@ use rusqlite::{Connection, Transaction}; use sql_support::open_database::{self, ConnectionInitializer}; +#[cfg(not(feature = "fakespot"))] /// The current database schema version. /// /// For any changes to the schema [`SQL`], please make sure to: @@ -17,6 +18,10 @@ use sql_support::open_database::{self, ConnectionInitializer}; /// the migration. pub const VERSION: u32 = 20; +#[cfg(feature = "fakespot")] +/// Database schema version for fakespot +pub const VERSION: u32 = 21; + /// The current Suggest database schema. pub const SQL: &str = " CREATE TABLE meta( @@ -151,7 +156,23 @@ impl ConnectionInitializer for SuggestConnectionInitializer { } fn init(&self, db: &Transaction<'_>) -> open_database::Result<()> { - Ok(db.execute_batch(SQL)?) + db.execute_batch(SQL)?; + + // FAKESPOT-TODO: The SNG will update this based on the results of their FTS experimentation + #[cfg(feature = "fakespot")] + db.execute_batch( + " +CREATE TABLE fakespot_custom_details( + suggestion_id INTEGER PRIMARY KEY, + fakespot_grade TEXT NOT NULL, + product_id TEXT NOT NULL, + rating REAL NOT NULL, + total_reviews INTEGER NOT NULL, + FOREIGN KEY(suggestion_id) REFERENCES suggestions(id) ON DELETE CASCADE +); + ", + )?; + Ok(()) } fn upgrade_from(&self, tx: &Transaction<'_>, version: u32) -> open_database::Result<()> { @@ -227,6 +248,29 @@ CREATE UNIQUE INDEX keywords_suggestion_id_rank ON keywords(suggestion_id, rank) )?; Ok(()) } + + // Migration for the fakespot data. This is not currently active for any users, it's + // only used for the tests. It's safe to alter the fakespot_custom_detail schema and + // update this migration as the project moves forward. + // + // Note: if we want to add a regular migration while the fakespot code is still behind + // a feature flag, insert it before this one and make fakespot the last migration. + #[cfg(feature = "fakespot")] + 20 => { + tx.execute_batch( + " +CREATE TABLE fakespot_custom_details( + suggestion_id INTEGER PRIMARY KEY, + fakespot_grade TEXT NOT NULL, + product_id TEXT NOT NULL, + rating REAL NOT NULL, + total_reviews INTEGER NOT NULL, + FOREIGN KEY(suggestion_id) REFERENCES suggestions(id) ON DELETE CASCADE +); + ", + )?; + Ok(()) + } _ => Err(open_database::Error::IncompatibleVersion(version)), } } diff --git a/components/suggest/src/store.rs b/components/suggest/src/store.rs index caec897487..9d16c20727 100644 --- a/components/suggest/src/store.rs +++ b/components/suggest/src/store.rs @@ -22,8 +22,8 @@ use crate::{ error::Error, provider::SuggestionProvider, rs::{ - Client, Record, RecordRequest, SuggestAttachment, SuggestRecord, SuggestRecordId, - SuggestRecordType, DEFAULT_RECORDS_TYPES, REMOTE_SETTINGS_COLLECTION, + Client, Record, RecordRequest, RemoteSettingsClient, SuggestAttachment, SuggestRecord, + SuggestRecordId, SuggestRecordType, DEFAULT_RECORDS_TYPES, }, Result, SuggestApiResult, Suggestion, SuggestionQuery, }; @@ -80,15 +80,14 @@ impl SuggestStoreBuilder { .clone() .ok_or_else(|| Error::SuggestStoreBuilder("data_path not specified".to_owned()))?; - let remote_settings_config = RemoteSettingsConfig { - server: inner.remote_settings_server.clone(), - bucket_name: inner.remote_settings_bucket_name.clone(), - server_url: None, - collection_name: REMOTE_SETTINGS_COLLECTION.into(), - }; - let settings_client = remote_settings::Client::new(remote_settings_config)?; + let client = RemoteSettingsClient::new( + inner.remote_settings_server.clone(), + inner.remote_settings_bucket_name.clone(), + None, + )?; + Ok(Arc::new(SuggestStore { - inner: SuggestStoreInner::new(data_path, settings_client), + inner: SuggestStoreInner::new(data_path, client), })) } } @@ -134,7 +133,7 @@ pub enum InterruptKind { /// later, while a desktop on a fast link might download the entire dataset /// on the first launch. pub struct SuggestStore { - inner: SuggestStoreInner, + inner: SuggestStoreInner, } impl SuggestStore { @@ -144,18 +143,20 @@ impl SuggestStore { path: &str, settings_config: Option, ) -> SuggestApiResult { - let settings_client = || -> Result<_> { - Ok(remote_settings::Client::new( - settings_config.unwrap_or_else(|| RemoteSettingsConfig { - server: None, - server_url: None, - bucket_name: None, - collection_name: REMOTE_SETTINGS_COLLECTION.into(), - }), - )?) - }()?; + let client = match settings_config { + Some(settings_config) => RemoteSettingsClient::new( + settings_config.server, + settings_config.bucket_name, + settings_config.server_url, + // Note: collection name is ignored, since we fetch from multiple collections + // (fakespot-suggest-products and quicksuggest). No consumer sets it to a + // non-default value anyways. + )?, + None => RemoteSettingsClient::new(None, None, None)?, + }; + Ok(Self { - inner: SuggestStoreInner::new(path.to_owned(), settings_client), + inner: SuggestStoreInner::new(path.to_owned(), client), }) } @@ -358,7 +359,7 @@ where constraints: &SuggestIngestionConstraints, ) -> Result<()> { let request = RecordRequest { - record_type: Some(ingest_record_type.to_string()), + record_type: ingest_record_type.to_string(), last_modified: dao .get_meta::(ingest_record_type.last_ingest_meta_key().as_str())?, limit: constraints.max_suggestions, @@ -492,6 +493,17 @@ where |dao, _| dao.put_global_config(&SuggestGlobalConfig::from(&config)), )?; } + #[cfg(feature = "fakespot")] + SuggestRecord::Fakespot => { + self.ingest_attachment( + &SuggestRecordType::Fakespot.last_ingest_meta_key(), + dao, + record, + |dao, record_id, suggestions| { + dao.insert_fakespot_suggestions(record_id, suggestions) + }, + )?; + } } } Ok(()) @@ -2151,4 +2163,31 @@ mod tests { Ok(()) } + + #[cfg(feature = "fakespot")] + #[test] + fn query_fakespot() -> anyhow::Result<()> { + before_each(); + + // FAKESPOT-TODO: Update these tests to test the new matching logic. + let store = TestStore::new(MockRemoteSettingsClient::default().with_record( + "fakespot-suggestions", + "fakespot-1", + json!([snowglobe_fakespot(), simpsons_fakespot()]), + )); + store.ingest(SuggestIngestionConstraints::default()); + assert_eq!( + store.fetch_suggestions(SuggestionQuery::fakespot("globe")), + vec![snowglobe_suggestion()], + ); + assert_eq!( + store.fetch_suggestions(SuggestionQuery::fakespot("simpsons")), + vec![simpsons_suggestion()], + ); + assert_eq!( + store.fetch_suggestions(SuggestionQuery::fakespot("snow")), + vec![simpsons_suggestion(), snowglobe_suggestion()], + ); + Ok(()) + } } diff --git a/components/suggest/src/suggest-fakespot.udl b/components/suggest/src/suggest-fakespot.udl new file mode 100644 index 0000000000..df6701ab24 --- /dev/null +++ b/components/suggest/src/suggest-fakespot.udl @@ -0,0 +1,196 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +[External="remote_settings"] +typedef extern RemoteSettingsConfig; + +[External="remote_settings"] +typedef extern RemoteSettingsServer; + +namespace suggest { + +boolean raw_suggestion_url_matches([ByRef] string raw_url, [ByRef] string url); + +}; + +[Error] +interface SuggestApiError { + // An operation was interrupted by calling `SuggestStore.interrupt()` + Interrupted(); + // The server requested a backoff after too many requests + Backoff(u64 seconds); + Network(string reason); + Other(string reason); +}; + +enum SuggestionProvider { + "Amp", + "Pocket", + "Wikipedia", + "Amo", + "Yelp", + "Mdn", + "Weather", + "AmpMobile", + "Fakespot", +}; + +[Enum] +interface Suggestion { + Amp( + string title, + string url, + string raw_url, + sequence? icon, + string? icon_mimetype, + string full_keyword, + i64 block_id, + string advertiser, + string iab_category, + string impression_url, + string click_url, + string raw_click_url, + f64 score + ); + Pocket( + string title, + string url, + f64 score, + boolean is_top_pick + ); + Wikipedia( + string title, + string url, + sequence? icon, + string? icon_mimetype, + string full_keyword + ); + Amo( + string title, + string url, + string icon_url, + string description, + string? rating, + i64 number_of_ratings, + string guid, + f64 score + ); + Yelp( + string url, + string title, + sequence? icon, + string? icon_mimetype, + f64 score, + boolean has_location_sign, + boolean subject_exact_match, + string location_param + ); + Mdn( + string title, + string url, + string description, + f64 score + ); + Weather( + f64 score + ); + Fakespot( + string fakespot_grade, + string product_id, + f64 rating, + string title, + i64 total_reviews, + string url, + f64 score + ); +}; + +dictionary SuggestionQuery { + string keyword; + sequence providers; + i32? limit = null; +}; + +dictionary SuggestIngestionConstraints { + u64? max_suggestions = null; + sequence? providers = null; + // Only ingest if the table `suggestions` is empty. + // + // This is indented to handle periodic updates. Consumers can schedule an ingest with + // `empty_only=true` on startup and a regular ingest with `empty_only=false` to run on a long periodic schedule (maybe + // once a day). This allows ingestion to normally be run at a slow, periodic rate. However, if + // there is a schema upgrade that causes the database to be thrown away, then the + // `empty_only=true` ingestion that runs on startup will repopulate it. + boolean empty_only = false; +}; + +dictionary SuggestGlobalConfig { + i32 show_less_frequently_cap; +}; + +[Enum] +interface SuggestProviderConfig { + Weather( + i32 min_keyword_length + ); +}; + +enum InterruptKind { + "Read", + "Write", + "ReadWrite", +}; + +interface SuggestStore { + [Throws=SuggestApiError] + constructor([ByRef] string path, optional RemoteSettingsConfig? settings_config = null); + + [Throws=SuggestApiError] + sequence query(SuggestionQuery query); + + [Throws=SuggestApiError] + void dismiss_suggestion(string raw_suggestion_url); + + [Throws=SuggestApiError] + void clear_dismissed_suggestions(); + + // Interrupt operations + // + // This is optional for backwards compatibility, but this is deprecated. Consumers should + // update their code to pass in a InterruptKind value. + void interrupt(optional InterruptKind? kind = null); + + [Throws=SuggestApiError] + void ingest(SuggestIngestionConstraints constraints); + + [Throws=SuggestApiError] + void clear(); + + [Throws=SuggestApiError] + SuggestGlobalConfig fetch_global_config(); + + [Throws=SuggestApiError] + SuggestProviderConfig? fetch_provider_config(SuggestionProvider provider); +}; + +interface SuggestStoreBuilder { + constructor(); + + [Self=ByArc] + SuggestStoreBuilder data_path(string path); + + // Deprecated: this is no longer used by the suggest component. + [Self=ByArc] + SuggestStoreBuilder cache_path(string path); + + [Self=ByArc] + SuggestStoreBuilder remote_settings_server(RemoteSettingsServer server); + + [Self=ByArc] + SuggestStoreBuilder remote_settings_bucket_name(string bucket_name); + + [Throws=SuggestApiError] + SuggestStore build(); +}; diff --git a/components/suggest/src/suggestion.rs b/components/suggest/src/suggestion.rs index c0b45524c7..015b8f8a2b 100644 --- a/components/suggest/src/suggestion.rs +++ b/components/suggest/src/suggestion.rs @@ -81,6 +81,16 @@ pub enum Suggestion { Weather { score: f64, }, + #[cfg(feature = "fakespot")] + Fakespot { + fakespot_grade: String, + product_id: String, + rating: f64, + title: String, + total_reviews: i64, + url: String, + score: f64, + }, } impl PartialOrd for Suggestion { diff --git a/components/suggest/src/testing/client.rs b/components/suggest/src/testing/client.rs index 9dea245aab..f171f5bf09 100644 --- a/components/suggest/src/testing/client.rs +++ b/components/suggest/src/testing/client.rs @@ -152,11 +152,8 @@ pub struct MockIcon { impl Client for MockRemoteSettingsClient { fn get_records(&self, request: RecordRequest) -> Result> { - let record_type = request.record_type.unwrap_or_else(|| { - panic!("MockRemoteSettingsClient.get_records: record_type required ") - }); // Note: limit and modified time are ignored - Ok(match self.records.get(&record_type) { + Ok(match self.records.get(&request.record_type) { Some(records) => records.clone(), None => vec![], }) diff --git a/components/suggest/src/testing/data.rs b/components/suggest/src/testing/data.rs index 01e7563432..5ba608ee86 100644 --- a/components/suggest/src/testing/data.rs +++ b/components/suggest/src/testing/data.rs @@ -449,3 +449,57 @@ pub fn multimatch_wiki_suggestion() -> Suggestion { full_keyword: "multimatch".into(), } } + +// Fakespot test data + +#[cfg(feature = "fakespot")] +pub fn snowglobe_fakespot() -> JsonValue { + json!({ + "fakespot_grade": "B", + "product_id": "amazon-ABC", + "rating": 4.7, + "score": 0.7, + "title": "Make Your Own Glitter Snow Globes", + "total_reviews": 152, + "url": "http://amazon.com/dp/ABC" + }) +} + +#[cfg(feature = "fakespot")] +pub fn snowglobe_suggestion() -> Suggestion { + Suggestion::Fakespot { + fakespot_grade: "B".into(), + product_id: "amazon-ABC".into(), + rating: 4.7, + title: "Make Your Own Glitter Snow Globes".into(), + total_reviews: 152, + url: "http://amazon.com/dp/ABC".into(), + score: 0.7, + } +} + +#[cfg(feature = "fakespot")] +pub fn simpsons_fakespot() -> JsonValue { + json!({ + "fakespot_grade": "A", + "product_id": "amazon-XYZ", + "rating": 4.9, + "score": 0.9, + "title": "The Simpsons: Skinner's Sense of Snow (DVD)", + "total_reviews": 14000, + "url": "http://amazon.com/dp/XYZ" + }) +} + +#[cfg(feature = "fakespot")] +pub fn simpsons_suggestion() -> Suggestion { + Suggestion::Fakespot { + fakespot_grade: "A".into(), + product_id: "amazon-XYZ".into(), + rating: 4.9, + title: "The Simpsons: Skinner's Sense of Snow (DVD)".into(), + total_reviews: 14000, + url: "http://amazon.com/dp/XYZ".into(), + score: 0.9, + } +} diff --git a/components/suggest/src/testing/mod.rs b/components/suggest/src/testing/mod.rs index 9e35272587..c14aa3b2d1 100644 --- a/components/suggest/src/testing/mod.rs +++ b/components/suggest/src/testing/mod.rs @@ -50,6 +50,8 @@ impl Suggestion { Self::Mdn { score, .. } => score, Self::Weather { score, .. } => score, Self::Wikipedia { .. } => panic!("with_score not valid for wikipedia suggestions"), + #[cfg(feature = "fakespot")] + Self::Fakespot { score, .. } => score, }; *current_score = score; self