/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis *file,Youcanobtainoneathttp://mozilla.org/MPL/2.0/.
*/
//! Crate-internal types for interacting with Remote Settings (`rs`). Types in //! this module describe records and attachments in the Suggest Remote Settings //! collection. //! //! To add a new suggestion `T` to this component, you'll generally need to: //! //! 1. Add a variant named `T` to [`SuggestRecord`]. The variant must have a //! `#[serde(rename)]` attribute that matches the suggestion record's //! `type` field. //! 2. Define a `DownloadedTSuggestion` type with the new suggestion's fields, //! matching their attachment's schema. Your new type must derive or //! implement [`serde::Deserialize`]. //! 3. Update the database schema in the [`schema`] module to store the new //! suggestion. //! 4. Add an `insert_t_suggestions()` method to [`db::SuggestDao`] that //! inserts `DownloadedTSuggestion`s into the database. //! 5. Update [`store::SuggestStoreInner::ingest()`] to download, deserialize, //! and store the new suggestion. //! 6. Add a variant named `T` to [`suggestion::Suggestion`], with the fields //! that you'd like to expose to the application. These can be the same //! fields as `DownloadedTSuggestion`, or slightly different, depending on //! what the application needs to show the suggestion. //! 7. Update the `Suggestion` enum definition in `suggest.udl` to match your //! new [`suggestion::Suggestion`] variant. //! 8. Update any [`db::SuggestDao`] methods that query the database to include //! the new suggestion in their results, and return `Suggestion::T` variants //! as needed.
use std::fmt;
use remote_settings::{Attachment, RemoteSettingsRecord}; use serde::{Deserialize, Deserializer};
/// A trait for a client that downloads suggestions from Remote Settings. /// /// This trait lets tests use a mock client. pub(crate) trait Client { /// Get all records from the server /// /// We use this plus client-side filtering rather than any server-side filtering, as /// recommended by the remote settings docs /// (https://remote-settings.readthedocs.io/en/stable/client-specifications.html). This is /// relatively inexpensive since we use a cache and don't fetch attachments until after the /// client-side filtering. /// /// Records that can't be parsed as [SuggestRecord] are ignored. fn get_records(&self, collection: Collection, dao: &mut SuggestDao) -> Result<Vec<Record>>;
/// Implements the [Client] trait using a real remote settings client pubstruct RemoteSettingsClient { // Create a separate client for each collection name
quicksuggest_client: remote_settings::RemoteSettings,
fakespot_client: remote_settings::RemoteSettings,
}
impl Client for RemoteSettingsClient { fn get_records(&self, collection: Collection, dao: &mut SuggestDao) -> Result<Vec<Record>> { // For now, handle the cache manually. Once 6328 is merged, we should be able to delegate // this to remote_settings. let client = self.client_for_collection(collection); let cache = dao.read_cached_rs_data(collection.name()); let last_modified = match &cache {
Some(response) => response.last_modified,
None => 0,
}; let response = match cache {
None => client.get_records()?,
Some(cache) => remote_settings::cache::merge_cache_and_response(
cache,
client.get_records_since(last_modified)?,
),
}; if last_modified != response.last_modified {
dao.write_cached_rs_data(collection.name(), &response);
}
/// A record in the Suggest Remote Settings collection. /// /// Most Suggest records don't carry inline fields except for `type`. /// Suggestions themselves are typically stored in each record's attachment. #[derive(Clone, Debug, Deserialize)] #[serde(tag = "type")] pub(crate) enum SuggestRecord { #[serde(rename = "icon")]
Icon, #[serde(rename = "data")]
AmpWikipedia, #[serde(rename = "amo-suggestions")]
Amo, #[serde(rename = "pocket-suggestions")]
Pocket, #[serde(rename = "yelp-suggestions")]
Yelp, #[serde(rename = "mdn-suggestions")]
Mdn, #[serde(rename = "weather")]
Weather, #[serde(rename = "configuration")]
GlobalConfig(DownloadedGlobalConfig), #[serde(rename = "amp-mobile-suggestions")]
AmpMobile, #[serde(rename = "fakespot-suggestions")]
Fakespot, #[serde(rename = "exposure-suggestions")]
Exposure(DownloadedExposureRecord), #[serde(rename = "geonames")]
Geonames,
}
/// Enum for the different record types that can be consumed. /// Extracting this from the serialization enum so that we can /// extend it to get type metadata. #[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] pubenum SuggestRecordType {
Icon,
AmpWikipedia,
Amo,
Pocket,
Yelp,
Mdn,
Weather,
GlobalConfig,
AmpMobile,
Fakespot,
Exposure,
Geonames,
}
impl SuggestRecordType { /// Get all record types to iterate over /// /// Currently only used by tests #[cfg(test)] pubfn all() -> &'static [SuggestRecordType] {
&[ Self::Icon, Self::AmpWikipedia, Self::Amo, Self::Pocket, Self::Yelp, Self::Mdn, Self::Weather, Self::GlobalConfig, Self::AmpMobile, Self::Fakespot, Self::Exposure, Self::Geonames,
]
}
/// Represents either a single value, or a list of values. This is used to /// deserialize downloaded attachments. #[derive(Clone, Debug, Deserialize)] #[serde(untagged)] enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
/// A downloaded Remote Settings attachment that contains suggestions. #[derive(Clone, Debug, Deserialize)] #[serde(transparent)] pub(crate) struct SuggestAttachment<T>(OneOrMany<T>);
impl<T> SuggestAttachment<T> { /// Returns a slice of suggestions to ingest from the downloaded attachment. pubfn suggestions(&self) -> &[T] { match &self.0 {
OneOrMany::One(value) => std::slice::from_ref(value),
OneOrMany::Many(values) => values,
}
}
}
/// The ID of a record in the Suggest Remote Settings collection. #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)] #[serde(transparent)] pub(crate) struct SuggestRecordId(String);
/// If this ID is for an icon record, extracts and returns the icon ID. /// /// The icon ID is the primary key for an ingested icon. Downloaded /// suggestions also reference these icon IDs, in /// [`DownloadedSuggestion::icon_id`]. pubfn as_icon_id(&self) -> Option<&str> { self.0.strip_prefix("icon-")
}
}
/// A Wikipedia suggestion to ingest from an AMP-Wikipedia attachment. #[derive(Clone, Debug, Default, Deserialize)] pub(crate) struct DownloadedWikipediaSuggestion { #[serde(flatten)] pub common_details: DownloadedSuggestionCommonDetails, #[serde(rename = "icon")] pub icon_id: String,
}
/// A suggestion to ingest from an AMP-Wikipedia attachment downloaded from /// Remote Settings. #[derive(Clone, Debug)] pub(crate) enum DownloadedAmpWikipediaSuggestion {
Amp(DownloadedAmpSuggestion),
Wikipedia(DownloadedWikipediaSuggestion),
}
impl DownloadedAmpWikipediaSuggestion { /// Returns the details that are common to AMP and Wikipedia suggestions. pubfn common_details(&self) -> &DownloadedSuggestionCommonDetails { matchself { Self::Amp(DownloadedAmpSuggestion { common_details, .. }) => common_details, Self::Wikipedia(DownloadedWikipediaSuggestion { common_details, .. }) => common_details,
}
}
/// Returns the provider of this suggestion. pubfn provider(&self) -> SuggestionProvider { matchself {
DownloadedAmpWikipediaSuggestion::Amp(_) => SuggestionProvider::Amp,
DownloadedAmpWikipediaSuggestion::Wikipedia(_) => SuggestionProvider::Wikipedia,
}
}
}
impl DownloadedSuggestionCommonDetails { /// Iterate over all keywords for this suggestion pubfn keywords(&self) -> impl Iterator<Item = AmpKeyword<'_>> { let full_keywords = self
.full_keywords
.iter()
.flat_map(|(full_keyword, repeat_for)| {
std::iter::repeat(Some(full_keyword.as_str())).take(*repeat_for)
})
.chain(std::iter::repeat(None)); // In case of insufficient full keywords, just fill in with infinite `None`s // self.keywords.iter().zip(full_keywords).enumerate().map( move |(i, (keyword, full_keyword))| AmpKeyword {
rank: i,
keyword,
full_keyword,
},
)
}
impl<'de> Deserialize<'de> for DownloadedAmpWikipediaSuggestion { fn deserialize<D>(
deserializer: D,
) -> std::result::Result<DownloadedAmpWikipediaSuggestion, D::Error> where
D: Deserializer<'de>,
{ // AMP and Wikipedia suggestions use the same schema. To separate them, // we use a "maybe tagged" outer enum with tagged and untagged variants, // and a "tagged" inner enum. // // Wikipedia suggestions will deserialize successfully into the tagged // variant. AMP suggestions will try the tagged variant, fail, and fall // back to the untagged variant. // // This approach works around serde-rs/serde#912.
/// An exposure suggestion record's inline data #[derive(Clone, Debug, Deserialize)] pub(crate) struct DownloadedExposureRecord { pub suggestion_type: String,
}
/// An exposure suggestion to ingest from an attachment #[derive(Clone, Debug, Deserialize)] pub(crate) struct DownloadedExposureSuggestion {
keywords: Vec<FullOrPrefixKeywords<String>>,
}
impl DownloadedExposureSuggestion { /// Iterate over all keywords for this suggestion. Iteration may contain /// duplicate keywords depending on the structure of the data, so do not /// assume keywords are unique. Duplicates are not filtered out because /// doing so would require O(number of keywords) space, and the number of /// keywords can be very large. If you are inserting into the store, rely on /// uniqueness constraints and use `INSERT OR IGNORE`. pubfn keywords(&self) -> impl Iterator<Item = String> + '_ { self.keywords.iter().flat_map(|e| e.keywords())
}
}
/// A single full keyword or a `(prefix, suffixes)` tuple representing multiple /// prefix keywords. Prefix keywords are enumerated by appending to `prefix` /// each possible prefix of each suffix, including the full suffix. The prefix /// is also enumerated by itself. Examples: /// /// `FullOrPrefixKeywords::Full("some full keyword")` /// => "some full keyword" /// /// `FullOrPrefixKeywords::Prefix(("sug", vec!["gest", "arplum"]))` /// => "sug" /// "sugg" /// "sugge" /// "sugges" /// "suggest" /// "suga" /// "sugar" /// "sugarp" /// "sugarpl" /// "sugarplu" /// "sugarplum" #[derive(Clone, Debug, Deserialize)] #[serde(untagged)] enum FullOrPrefixKeywords<T> {
Full(T),
Prefix((T, Vec<T>)),
}
/// Global Suggest configuration data to ingest from a configuration record #[derive(Clone, Debug, Deserialize)] pub(crate) struct DownloadedGlobalConfig { pub configuration: DownloadedGlobalConfigInner,
} #[derive(Clone, Debug, Deserialize)] pub(crate) struct DownloadedGlobalConfigInner { /// The maximum number of times the user can click "Show less frequently" /// for a suggestion in the UI. pub show_less_frequently_cap: i32,
}
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.