use std::fmt;
use remote_settings::{Attachment, RemoteSettingsRecord};
use serde::{Deserialize, Deserializer};
use crate::{db::SuggestDao, error::Error, provider::SuggestionProvider, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Collection {
Quicksuggest,
Fakespot,
}
impl Collection {
pub fn name(&self) -> &'static str {
match self {
Self::Quicksuggest => "quicksuggest",
Self::Fakespot => "fakespot-suggest-products",
}
}
}
pub(crate) trait Client {
fn get_records(&self, collection: Collection, dao: &mut SuggestDao) -> Result<Vec<Record>>;
fn download_attachment(&self, record: &Record) -> Result<Vec<u8>>;
}
pub struct RemoteSettingsClient {
quicksuggest_client: remote_settings::RemoteSettings,
fakespot_client: remote_settings::RemoteSettings,
}
impl RemoteSettingsClient {
pub fn new(
server: Option<remote_settings::RemoteSettingsServer>,
bucket_name: Option<String>,
server_url: Option<String>,
) -> Result<Self> {
Ok(Self {
quicksuggest_client: remote_settings::RemoteSettings::new(
remote_settings::RemoteSettingsConfig {
server: server.clone(),
bucket_name: bucket_name.clone(),
collection_name: "quicksuggest".to_owned(),
server_url: server_url.clone(),
},
)?,
fakespot_client: remote_settings::RemoteSettings::new(
remote_settings::RemoteSettingsConfig {
server,
bucket_name,
collection_name: "fakespot-suggest-products".to_owned(),
server_url,
},
)?,
})
}
fn client_for_collection(&self, collection: Collection) -> &remote_settings::RemoteSettings {
match collection {
Collection::Fakespot => &self.fakespot_client,
Collection::Quicksuggest => &self.quicksuggest_client,
}
}
}
impl Client for RemoteSettingsClient {
fn get_records(&self, collection: Collection, dao: &mut SuggestDao) -> Result<Vec<Record>> {
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);
}
Ok(response
.records
.into_iter()
.filter_map(|r| Record::new(r, collection).ok())
.collect())
}
fn download_attachment(&self, record: &Record) -> Result<Vec<u8>> {
match &record.attachment {
Some(a) => Ok(self
.client_for_collection(record.collection)
.get_attachment(&a.location)?),
None => Err(Error::MissingAttachment(record.id.to_string())),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct Record {
pub id: SuggestRecordId,
pub last_modified: u64,
pub attachment: Option<Attachment>,
pub payload: SuggestRecord,
pub collection: Collection,
}
impl Record {
pub fn new(record: RemoteSettingsRecord, collection: Collection) -> Result<Self> {
Ok(Self {
id: SuggestRecordId::new(record.id),
last_modified: record.last_modified,
attachment: record.attachment,
payload: serde_json::from_value(serde_json::Value::Object(record.fields))?,
collection,
})
}
pub fn record_type(&self) -> SuggestRecordType {
(&self.payload).into()
}
}
#[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,
}
#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub enum SuggestRecordType {
Icon,
AmpWikipedia,
Amo,
Pocket,
Yelp,
Mdn,
Weather,
GlobalConfig,
AmpMobile,
Fakespot,
Exposure,
Geonames,
}
impl From<&SuggestRecord> for SuggestRecordType {
fn from(suggest_record: &SuggestRecord) -> Self {
match suggest_record {
SuggestRecord::Amo => Self::Amo,
SuggestRecord::AmpWikipedia => Self::AmpWikipedia,
SuggestRecord::Icon => Self::Icon,
SuggestRecord::Mdn => Self::Mdn,
SuggestRecord::Pocket => Self::Pocket,
SuggestRecord::Weather => Self::Weather,
SuggestRecord::Yelp => Self::Yelp,
SuggestRecord::GlobalConfig(_) => Self::GlobalConfig,
SuggestRecord::AmpMobile => Self::AmpMobile,
SuggestRecord::Fakespot => Self::Fakespot,
SuggestRecord::Exposure(_) => Self::Exposure,
SuggestRecord::Geonames => Self::Geonames,
}
}
}
impl fmt::Display for SuggestRecordType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl SuggestRecordType {
#[cfg(test)]
pub fn 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,
]
}
pub fn as_str(&self) -> &str {
match self {
Self::Icon => "icon",
Self::AmpWikipedia => "data",
Self::Amo => "amo-suggestions",
Self::Pocket => "pocket-suggestions",
Self::Yelp => "yelp-suggestions",
Self::Mdn => "mdn-suggestions",
Self::Weather => "weather",
Self::GlobalConfig => "configuration",
Self::AmpMobile => "amp-mobile-suggestions",
Self::Fakespot => "fakespot-suggestions",
Self::Exposure => "exposure-suggestions",
Self::Geonames => "geonames",
}
}
pub fn collection(&self) -> Collection {
match self {
Self::Fakespot => Collection::Fakespot,
_ => Collection::Quicksuggest,
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
#[derive(Clone, Debug, Deserialize)]
#[serde(transparent)]
pub(crate) struct SuggestAttachment<T>(OneOrMany<T>);
impl<T> SuggestAttachment<T> {
pub fn suggestions(&self) -> &[T] {
match &self.0 {
OneOrMany::One(value) => std::slice::from_ref(value),
OneOrMany::Many(values) => values,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub(crate) struct SuggestRecordId(String);
impl SuggestRecordId {
pub fn new(id: String) -> Self {
Self(id)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_icon_id(&self) -> Option<&str> {
self.0.strip_prefix("icon-")
}
}
impl fmt::Display for SuggestRecordId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub(crate) struct DownloadedSuggestionCommonDetails {
pub keywords: Vec<String>,
pub title: String,
pub url: String,
pub score: Option<f64>,
#[serde(default)]
pub full_keywords: Vec<(String, usize)>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub(crate) struct DownloadedAmpSuggestion {
#[serde(flatten)]
pub common_details: DownloadedSuggestionCommonDetails,
pub advertiser: String,
#[serde(rename = "id")]
pub block_id: i32,
pub iab_category: String,
pub click_url: String,
pub impression_url: String,
#[serde(rename = "icon")]
pub icon_id: String,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub(crate) struct DownloadedWikipediaSuggestion {
#[serde(flatten)]
pub common_details: DownloadedSuggestionCommonDetails,
#[serde(rename = "icon")]
pub icon_id: String,
}
#[derive(Clone, Debug)]
pub(crate) enum DownloadedAmpWikipediaSuggestion {
Amp(DownloadedAmpSuggestion),
Wikipedia(DownloadedWikipediaSuggestion),
}
impl DownloadedAmpWikipediaSuggestion {
pub fn common_details(&self) -> &DownloadedSuggestionCommonDetails {
match self {
Self::Amp(DownloadedAmpSuggestion { common_details, .. }) => common_details,
Self::Wikipedia(DownloadedWikipediaSuggestion { common_details, .. }) => common_details,
}
}
pub fn provider(&self) -> SuggestionProvider {
match self {
DownloadedAmpWikipediaSuggestion::Amp(_) => SuggestionProvider::Amp,
DownloadedAmpWikipediaSuggestion::Wikipedia(_) => SuggestionProvider::Wikipedia,
}
}
}
impl DownloadedSuggestionCommonDetails {
pub fn 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)); self.keywords.iter().zip(full_keywords).enumerate().map(
move |(i, (keyword, full_keyword))| AmpKeyword {
rank: i,
keyword,
full_keyword,
},
)
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct AmpKeyword<'a> {
pub rank: usize,
pub keyword: &'a str,
pub full_keyword: Option<&'a str>,
}
impl<'de> Deserialize<'de> for DownloadedAmpWikipediaSuggestion {
fn deserialize<D>(
deserializer: D,
) -> std::result::Result<DownloadedAmpWikipediaSuggestion, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum MaybeTagged {
Tagged(Tagged),
Untagged(DownloadedAmpSuggestion),
}
#[derive(Deserialize)]
#[serde(tag = "advertiser")]
enum Tagged {
#[serde(rename = "Wikipedia")]
Wikipedia(DownloadedWikipediaSuggestion),
}
Ok(match MaybeTagged::deserialize(deserializer)? {
MaybeTagged::Tagged(Tagged::Wikipedia(wikipedia)) => Self::Wikipedia(wikipedia),
MaybeTagged::Untagged(amp) => Self::Amp(amp),
})
}
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedAmoSuggestion {
pub description: String,
pub url: String,
pub guid: String,
#[serde(rename = "icon")]
pub icon_url: String,
pub rating: Option<String>,
pub number_of_ratings: i64,
pub title: String,
pub keywords: Vec<String>,
pub score: f64,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedPocketSuggestion {
pub url: String,
pub title: String,
#[serde(rename = "lowConfidenceKeywords")]
pub low_confidence_keywords: Vec<String>,
#[serde(rename = "highConfidenceKeywords")]
pub high_confidence_keywords: Vec<String>,
pub score: f64,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedYelpLocationSign {
pub keyword: String,
#[serde(rename = "needLocation")]
pub need_location: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedYelpSuggestion {
pub subjects: Vec<String>,
#[serde(rename = "preModifiers")]
pub pre_modifiers: Vec<String>,
#[serde(rename = "postModifiers")]
pub post_modifiers: Vec<String>,
#[serde(rename = "locationSigns")]
pub location_signs: Vec<DownloadedYelpLocationSign>,
#[serde(rename = "yelpModifiers")]
pub yelp_modifiers: Vec<String>,
#[serde(rename = "icon")]
pub icon_id: String,
pub score: f64,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedMdnSuggestion {
pub url: String,
pub title: String,
pub description: String,
pub keywords: Vec<String>,
pub score: f64,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedFakespotSuggestion {
pub fakespot_grade: String,
pub product_id: String,
pub keywords: String,
pub product_type: String,
pub rating: f64,
pub score: f64,
pub title: String,
pub total_reviews: i64,
pub url: String,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedExposureRecord {
pub suggestion_type: String,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedExposureSuggestion {
keywords: Vec<FullOrPrefixKeywords<String>>,
}
impl DownloadedExposureSuggestion {
pub fn keywords(&self) -> impl Iterator<Item = String> + '_ {
self.keywords.iter().flat_map(|e| e.keywords())
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
enum FullOrPrefixKeywords<T> {
Full(T),
Prefix((T, Vec<T>)),
}
impl<T> From<T> for FullOrPrefixKeywords<T> {
fn from(full_keyword: T) -> Self {
Self::Full(full_keyword)
}
}
impl<T> From<(T, Vec<T>)> for FullOrPrefixKeywords<T> {
fn from(prefix_suffixes: (T, Vec<T>)) -> Self {
Self::Prefix(prefix_suffixes)
}
}
impl FullOrPrefixKeywords<String> {
pub fn keywords(&self) -> Box<dyn Iterator<Item = String> + '_> {
match self {
FullOrPrefixKeywords::Full(kw) => Box::new(std::iter::once(kw.to_owned())),
FullOrPrefixKeywords::Prefix((prefix, suffixes)) => Box::new(
std::iter::once(prefix.to_owned()).chain(suffixes.iter().flat_map(|suffix| {
let mut kw = prefix.clone();
suffix.chars().map(move |c| {
kw.push(c);
kw.clone()
})
})),
),
}
}
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedGlobalConfig {
pub configuration: DownloadedGlobalConfigInner,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedGlobalConfigInner {
pub show_less_frequently_cap: i32,
}
pub(crate) fn deserialize_f64_or_default<'de, D>(
deserializer: D,
) -> std::result::Result<f64, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(|s| s.parse().ok().unwrap_or_default())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_full_keywords() {
let suggestion = DownloadedAmpWikipediaSuggestion::Amp(DownloadedAmpSuggestion {
common_details: DownloadedSuggestionCommonDetails {
keywords: vec![
String::from("f"),
String::from("fo"),
String::from("foo"),
String::from("foo b"),
String::from("foo ba"),
String::from("foo bar"),
],
full_keywords: vec![(String::from("foo"), 3), (String::from("foo bar"), 3)],
..DownloadedSuggestionCommonDetails::default()
},
..DownloadedAmpSuggestion::default()
});
assert_eq!(
Vec::from_iter(suggestion.common_details().keywords()),
vec![
AmpKeyword {
rank: 0,
keyword: "f",
full_keyword: Some("foo"),
},
AmpKeyword {
rank: 1,
keyword: "fo",
full_keyword: Some("foo"),
},
AmpKeyword {
rank: 2,
keyword: "foo",
full_keyword: Some("foo"),
},
AmpKeyword {
rank: 3,
keyword: "foo b",
full_keyword: Some("foo bar"),
},
AmpKeyword {
rank: 4,
keyword: "foo ba",
full_keyword: Some("foo bar"),
},
AmpKeyword {
rank: 5,
keyword: "foo bar",
full_keyword: Some("foo bar"),
},
],
);
}
#[test]
fn test_missing_full_keywords() {
let suggestion = DownloadedAmpWikipediaSuggestion::Amp(DownloadedAmpSuggestion {
common_details: DownloadedSuggestionCommonDetails {
keywords: vec![
String::from("f"),
String::from("fo"),
String::from("foo"),
String::from("foo b"),
String::from("foo ba"),
String::from("foo bar"),
],
full_keywords: vec![(String::from("foo"), 3)],
..DownloadedSuggestionCommonDetails::default()
},
..DownloadedAmpSuggestion::default()
});
assert_eq!(
Vec::from_iter(suggestion.common_details().keywords()),
vec![
AmpKeyword {
rank: 0,
keyword: "f",
full_keyword: Some("foo"),
},
AmpKeyword {
rank: 1,
keyword: "fo",
full_keyword: Some("foo"),
},
AmpKeyword {
rank: 2,
keyword: "foo",
full_keyword: Some("foo"),
},
AmpKeyword {
rank: 3,
keyword: "foo b",
full_keyword: None,
},
AmpKeyword {
rank: 4,
keyword: "foo ba",
full_keyword: None,
},
AmpKeyword {
rank: 5,
keyword: "foo bar",
full_keyword: None,
},
],
);
}
fn full_or_prefix_keywords_to_owned(
kws: Vec<FullOrPrefixKeywords<&str>>,
) -> Vec<FullOrPrefixKeywords<String>> {
kws.iter()
.map(|val| match val {
FullOrPrefixKeywords::Full(s) => FullOrPrefixKeywords::Full(s.to_string()),
FullOrPrefixKeywords::Prefix((prefix, suffixes)) => FullOrPrefixKeywords::Prefix((
prefix.to_string(),
suffixes.iter().map(|s| s.to_string()).collect(),
)),
})
.collect()
}
#[test]
fn test_exposure_keywords() {
let suggestion = DownloadedExposureSuggestion {
keywords: full_or_prefix_keywords_to_owned(vec![
"no suffixes".into(),
("empty suffixes", vec![]).into(),
("empty string suffix", vec![""]).into(),
("choco", vec!["", "bo", "late"]).into(),
"duplicate 1".into(),
"duplicate 1".into(),
("dup", vec!["licate 1", "licate 2"]).into(),
("dup", vec!["lo", "licate 2", "licate 3"]).into(),
("duplic", vec!["ate 3", "ar", "ate 4"]).into(),
("du", vec!["plicate 4", "plicate 5", "nk"]).into(),
]),
};
assert_eq!(
Vec::from_iter(suggestion.keywords()),
vec![
"no suffixes",
"empty suffixes",
"empty string suffix",
"choco",
"chocob",
"chocobo",
"chocol",
"chocola",
"chocolat",
"chocolate",
"duplicate 1",
"duplicate 1",
"dup",
"dupl",
"dupli",
"duplic",
"duplica",
"duplicat",
"duplicate",
"duplicate ",
"duplicate 1",
"dupl",
"dupli",
"duplic",
"duplica",
"duplicat",
"duplicate",
"duplicate ",
"duplicate 2",
"dup",
"dupl",
"duplo",
"dupl",
"dupli",
"duplic",
"duplica",
"duplicat",
"duplicate",
"duplicate ",
"duplicate 2",
"dupl",
"dupli",
"duplic",
"duplica",
"duplicat",
"duplicate",
"duplicate ",
"duplicate 3",
"duplic",
"duplica",
"duplicat",
"duplicate",
"duplicate ",
"duplicate 3",
"duplica",
"duplicar",
"duplica",
"duplicat",
"duplicate",
"duplicate ",
"duplicate 4",
"du",
"dup",
"dupl",
"dupli",
"duplic",
"duplica",
"duplicat",
"duplicate",
"duplicate ",
"duplicate 4",
"dup",
"dupl",
"dupli",
"duplic",
"duplica",
"duplicat",
"duplicate",
"duplicate ",
"duplicate 5",
"dun",
"dunk",
],
);
}
}