1use std::{fmt, sync::Arc};
35
36use remote_settings::{
37 Attachment, RemoteSettingsClient, RemoteSettingsError, RemoteSettingsRecord,
38 RemoteSettingsService,
39};
40use serde::{Deserialize, Serialize};
41use serde_json::{Map, Value};
42
43use crate::{error::Error, query::full_keywords_to_fts_content, Result};
44use rusqlite::{types::ToSqlOutput, ToSql};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum Collection {
48 Amp,
49 Fakespot,
50 Other,
51}
52
53impl Collection {
54 pub fn name(&self) -> &'static str {
55 match self {
56 Self::Amp => "quicksuggest-amp",
57 Self::Fakespot => "fakespot-suggest-products",
58 Self::Other => "quicksuggest-other",
59 }
60 }
61}
62
63pub(crate) trait Client {
67 fn get_records(&self, collection: Collection) -> Result<Vec<Record>>;
77
78 fn download_attachment(&self, record: &Record) -> Result<Vec<u8>>;
79}
80
81pub struct SuggestRemoteSettingsClient {
83 amp_client: Arc<RemoteSettingsClient>,
85 other_client: Arc<RemoteSettingsClient>,
86 fakespot_client: Arc<RemoteSettingsClient>,
87}
88
89impl SuggestRemoteSettingsClient {
90 pub fn new(rs_service: &RemoteSettingsService) -> Self {
91 Self {
92 amp_client: rs_service.make_client(Collection::Amp.name().to_owned()),
93 other_client: rs_service.make_client(Collection::Other.name().to_owned()),
94 fakespot_client: rs_service.make_client(Collection::Fakespot.name().to_owned()),
95 }
96 }
97
98 fn client_for_collection(&self, collection: Collection) -> &RemoteSettingsClient {
99 match collection {
100 Collection::Amp => &self.amp_client,
101 Collection::Other => &self.other_client,
102 Collection::Fakespot => &self.fakespot_client,
103 }
104 }
105}
106
107impl Client for SuggestRemoteSettingsClient {
108 fn get_records(&self, collection: Collection) -> Result<Vec<Record>> {
109 let client = self.client_for_collection(collection);
110 client.sync()?;
111 let response = client.get_records(false);
112 match response {
113 Some(r) => Ok(r
114 .into_iter()
115 .filter_map(|r| Record::new(r, collection).ok())
116 .collect()),
117 None => Err(Error::RemoteSettings(RemoteSettingsError::Other {
118 reason: "Unable to get records".to_owned(),
119 })),
120 }
121 }
122
123 fn download_attachment(&self, record: &Record) -> Result<Vec<u8>> {
124 let converted_record: RemoteSettingsRecord = record.clone().into();
125 match &record.attachment {
126 Some(_) => Ok(self
127 .client_for_collection(record.collection)
128 .get_attachment(&converted_record)?),
129 None => Err(Error::MissingAttachment(record.id.to_string())),
130 }
131 }
132}
133
134#[derive(Clone, Debug)]
138pub(crate) struct Record {
139 pub id: SuggestRecordId,
140 pub last_modified: u64,
141 pub attachment: Option<Attachment>,
142 pub payload: SuggestRecord,
143 pub collection: Collection,
144}
145
146impl Record {
147 pub fn new(record: RemoteSettingsRecord, collection: Collection) -> Result<Self> {
148 Ok(Self {
149 id: SuggestRecordId::new(record.id),
150 last_modified: record.last_modified,
151 attachment: record.attachment,
152 payload: serde_json::from_value(serde_json::Value::Object(record.fields))?,
153 collection,
154 })
155 }
156
157 pub fn record_type(&self) -> SuggestRecordType {
158 (&self.payload).into()
159 }
160}
161
162impl From<Record> for RemoteSettingsRecord {
163 fn from(record: Record) -> Self {
164 RemoteSettingsRecord {
165 id: record.id.to_string(),
166 last_modified: record.last_modified,
167 deleted: false,
168 attachment: record.attachment.clone(),
169 fields: record.payload.to_json_map(),
170 }
171 }
172}
173
174#[derive(Clone, Debug, Deserialize, Serialize)]
179#[serde(tag = "type")]
180pub(crate) enum SuggestRecord {
181 #[serde(rename = "icon")]
182 Icon,
183 #[serde(rename = "amp")]
184 Amp,
185 #[serde(rename = "wikipedia")]
186 Wikipedia,
187 #[serde(rename = "amo-suggestions")]
188 Amo,
189 #[serde(rename = "yelp-suggestions")]
190 Yelp,
191 #[serde(rename = "mdn-suggestions")]
192 Mdn,
193 #[serde(rename = "weather")]
194 Weather,
195 #[serde(rename = "configuration")]
196 GlobalConfig(DownloadedGlobalConfig),
197 #[serde(rename = "fakespot-suggestions")]
198 Fakespot,
199 #[serde(rename = "dynamic-suggestions")]
200 Dynamic(DownloadedDynamicRecord),
201 #[serde(rename = "geonames-2")] Geonames,
203 #[serde(rename = "geonames-alternates")]
204 GeonamesAlternates,
205}
206
207impl SuggestRecord {
208 fn to_json_map(&self) -> Map<String, Value> {
209 match serde_json::to_value(self) {
210 Ok(Value::Object(map)) => map,
211 _ => unreachable!(),
212 }
213 }
214}
215
216#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
220pub enum SuggestRecordType {
221 Icon,
222 Amp,
223 Wikipedia,
224 Amo,
225 Yelp,
226 Mdn,
227 Weather,
228 GlobalConfig,
229 Fakespot,
230 Dynamic,
231 Geonames,
232 GeonamesAlternates,
233}
234
235impl From<&SuggestRecord> for SuggestRecordType {
236 fn from(suggest_record: &SuggestRecord) -> Self {
237 match suggest_record {
238 SuggestRecord::Amo => Self::Amo,
239 SuggestRecord::Amp => Self::Amp,
240 SuggestRecord::Wikipedia => Self::Wikipedia,
241 SuggestRecord::Icon => Self::Icon,
242 SuggestRecord::Mdn => Self::Mdn,
243 SuggestRecord::Weather => Self::Weather,
244 SuggestRecord::Yelp => Self::Yelp,
245 SuggestRecord::GlobalConfig(_) => Self::GlobalConfig,
246 SuggestRecord::Fakespot => Self::Fakespot,
247 SuggestRecord::Dynamic(_) => Self::Dynamic,
248 SuggestRecord::Geonames => Self::Geonames,
249 SuggestRecord::GeonamesAlternates => Self::GeonamesAlternates,
250 }
251 }
252}
253
254impl fmt::Display for SuggestRecordType {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 write!(f, "{}", self.as_str())
257 }
258}
259
260impl ToSql for SuggestRecordType {
261 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
262 Ok(ToSqlOutput::from(self.as_str()))
263 }
264}
265
266impl SuggestRecordType {
267 #[cfg(test)]
271 pub fn all() -> &'static [SuggestRecordType] {
272 &[
273 Self::Icon,
274 Self::Amp,
275 Self::Wikipedia,
276 Self::Amo,
277 Self::Yelp,
278 Self::Mdn,
279 Self::Weather,
280 Self::GlobalConfig,
281 Self::Fakespot,
282 Self::Dynamic,
283 Self::Geonames,
284 Self::GeonamesAlternates,
285 ]
286 }
287
288 pub fn as_str(&self) -> &str {
289 match self {
290 Self::Icon => "icon",
291 Self::Amp => "amp",
292 Self::Wikipedia => "wikipedia",
293 Self::Amo => "amo-suggestions",
294 Self::Yelp => "yelp-suggestions",
295 Self::Mdn => "mdn-suggestions",
296 Self::Weather => "weather",
297 Self::GlobalConfig => "configuration",
298 Self::Fakespot => "fakespot-suggestions",
299 Self::Dynamic => "dynamic-suggestions",
300 Self::Geonames => "geonames-2",
301 Self::GeonamesAlternates => "geonames-alternates",
302 }
303 }
304}
305
306#[derive(Clone, Debug, Deserialize)]
309#[serde(untagged)]
310enum OneOrMany<T> {
311 One(T),
312 Many(Vec<T>),
313}
314
315#[derive(Clone, Debug, Deserialize)]
317#[serde(transparent)]
318pub(crate) struct SuggestAttachment<T>(OneOrMany<T>);
319
320impl<T> SuggestAttachment<T> {
321 pub fn suggestions(&self) -> &[T] {
323 match &self.0 {
324 OneOrMany::One(value) => std::slice::from_ref(value),
325 OneOrMany::Many(values) => values,
326 }
327 }
328}
329
330#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
332#[serde(transparent)]
333pub(crate) struct SuggestRecordId(String);
334
335impl SuggestRecordId {
336 pub fn new(id: String) -> Self {
337 Self(id)
338 }
339
340 pub fn as_str(&self) -> &str {
341 &self.0
342 }
343
344 pub fn as_icon_id(&self) -> Option<&str> {
350 self.0.strip_prefix("icon-")
351 }
352}
353
354impl fmt::Display for SuggestRecordId {
355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356 write!(f, "{}", self.0)
357 }
358}
359
360#[derive(Clone, Debug, Default, Deserialize)]
362pub(crate) struct DownloadedAmpSuggestion {
363 pub keywords: Vec<String>,
364 pub title: String,
365 pub url: String,
366 pub score: Option<f64>,
367 #[serde(default)]
368 pub full_keywords: Vec<(String, usize)>,
369 pub advertiser: String,
370 #[serde(rename = "id")]
371 pub block_id: i32,
372 pub iab_category: String,
373 pub click_url: String,
374 pub impression_url: String,
375 #[serde(rename = "icon")]
376 pub icon_id: String,
377}
378
379#[derive(Clone, Debug, Default, Deserialize)]
381pub(crate) struct DownloadedWikipediaSuggestion {
382 pub keywords: Vec<String>,
383 pub title: String,
384 pub url: String,
385 pub score: Option<f64>,
386 #[serde(default)]
387 pub full_keywords: Vec<(String, usize)>,
388 #[serde(rename = "icon")]
389 pub icon_id: String,
390}
391
392pub fn iterate_keywords<'a>(
394 keywords: &'a [String],
395 full_keywords: &'a [(String, usize)],
396) -> impl Iterator<Item = AmpKeyword<'a>> {
397 let full_keywords_iter = full_keywords
398 .iter()
399 .flat_map(|(full_keyword, repeat_for)| {
400 std::iter::repeat_n(Some(full_keyword.as_str()), *repeat_for)
401 })
402 .chain(std::iter::repeat(None)); keywords
405 .iter()
406 .zip(full_keywords_iter)
407 .enumerate()
408 .map(move |(i, (keyword, full_keyword))| AmpKeyword {
409 rank: i,
410 keyword,
411 full_keyword,
412 })
413}
414
415impl DownloadedAmpSuggestion {
416 pub fn keywords(&self) -> impl Iterator<Item = AmpKeyword<'_>> {
417 iterate_keywords(&self.keywords, &self.full_keywords)
418 }
419
420 pub fn full_keywords_fts_column(&self) -> String {
421 full_keywords_to_fts_content(self.full_keywords.iter().map(|(s, _)| s.as_str()))
422 }
423}
424
425impl DownloadedWikipediaSuggestion {
426 pub fn keywords(&self) -> impl Iterator<Item = AmpKeyword<'_>> {
427 iterate_keywords(&self.keywords, &self.full_keywords)
428 }
429}
430
431#[derive(Debug, PartialEq, Eq)]
432pub(crate) struct AmpKeyword<'a> {
433 pub rank: usize,
434 pub keyword: &'a str,
435 pub full_keyword: Option<&'a str>,
436}
437
438#[derive(Clone, Debug, Deserialize)]
440pub(crate) struct DownloadedAmoSuggestion {
441 pub description: String,
442 pub url: String,
443 pub guid: String,
444 #[serde(rename = "icon")]
445 pub icon_url: String,
446 pub rating: Option<String>,
447 pub number_of_ratings: i64,
448 pub title: String,
449 pub keywords: Vec<String>,
450 pub score: f64,
451}
452#[derive(Clone, Debug, Deserialize)]
454#[serde(untagged)]
455pub enum DownloadedYelpLocationSign {
456 V1 { keyword: String },
457 V2(String),
458}
459impl ToSql for DownloadedYelpLocationSign {
460 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
461 let keyword = match self {
462 DownloadedYelpLocationSign::V1 { keyword } => keyword,
463 DownloadedYelpLocationSign::V2(keyword) => keyword,
464 };
465 Ok(ToSqlOutput::from(keyword.as_str()))
466 }
467}
468#[derive(Clone, Debug, Deserialize)]
470pub(crate) struct DownloadedYelpSuggestion {
471 pub subjects: Vec<String>,
472 #[serde(rename = "businessSubjects")]
473 pub business_subjects: Option<Vec<String>>,
474 #[serde(rename = "preModifiers")]
475 pub pre_modifiers: Vec<String>,
476 #[serde(rename = "postModifiers")]
477 pub post_modifiers: Vec<String>,
478 #[serde(rename = "locationSigns")]
479 pub location_signs: Vec<DownloadedYelpLocationSign>,
480 #[serde(rename = "yelpModifiers")]
481 pub yelp_modifiers: Vec<String>,
482 #[serde(rename = "icon")]
483 pub icon_id: String,
484 pub score: f64,
485}
486
487#[derive(Clone, Debug, Deserialize)]
489pub(crate) struct DownloadedMdnSuggestion {
490 pub url: String,
491 pub title: String,
492 pub description: String,
493 pub keywords: Vec<String>,
494 pub score: f64,
495}
496
497#[derive(Clone, Debug, Deserialize)]
499pub(crate) struct DownloadedFakespotSuggestion {
500 pub fakespot_grade: String,
501 pub product_id: String,
502 pub keywords: String,
503 pub product_type: String,
504 pub rating: f64,
505 pub score: f64,
506 pub title: String,
507 pub total_reviews: i64,
508 pub url: String,
509}
510
511#[derive(Clone, Debug, Deserialize, Serialize)]
513pub(crate) struct DownloadedDynamicRecord {
514 pub suggestion_type: String,
515 pub score: Option<f64>,
516}
517
518#[derive(Clone, Debug, Deserialize)]
520pub(crate) struct DownloadedDynamicSuggestion {
521 keywords: Vec<FullOrPrefixKeywords<String>>,
522 pub dismissal_key: Option<String>,
523 pub data: Option<Value>,
524}
525
526impl DownloadedDynamicSuggestion {
527 pub fn keywords(&self) -> impl Iterator<Item = String> + '_ {
534 self.keywords.iter().flat_map(|e| e.keywords())
535 }
536}
537
538#[derive(Clone, Debug, Deserialize)]
559#[serde(untagged)]
560enum FullOrPrefixKeywords<T> {
561 Full(T),
562 Prefix((T, Vec<T>)),
563}
564
565impl<T> From<T> for FullOrPrefixKeywords<T> {
566 fn from(full_keyword: T) -> Self {
567 Self::Full(full_keyword)
568 }
569}
570
571impl<T> From<(T, Vec<T>)> for FullOrPrefixKeywords<T> {
572 fn from(prefix_suffixes: (T, Vec<T>)) -> Self {
573 Self::Prefix(prefix_suffixes)
574 }
575}
576
577impl FullOrPrefixKeywords<String> {
578 pub fn keywords(&self) -> Box<dyn Iterator<Item = String> + '_> {
579 match self {
580 FullOrPrefixKeywords::Full(kw) => Box::new(std::iter::once(kw.to_owned())),
581 FullOrPrefixKeywords::Prefix((prefix, suffixes)) => Box::new(
582 std::iter::once(prefix.to_owned()).chain(suffixes.iter().flat_map(|suffix| {
583 let mut kw = prefix.clone();
584 suffix.chars().map(move |c| {
585 kw.push(c);
586 kw.clone()
587 })
588 })),
589 ),
590 }
591 }
592}
593
594#[derive(Clone, Debug, Deserialize, Serialize)]
596pub(crate) struct DownloadedGlobalConfig {
597 pub configuration: DownloadedGlobalConfigInner,
598}
599#[derive(Clone, Debug, Deserialize, Serialize)]
600pub(crate) struct DownloadedGlobalConfigInner {
601 pub show_less_frequently_cap: i32,
604}
605
606#[cfg(test)]
607mod test {
608 use super::*;
609
610 #[test]
611 fn test_full_keywords() {
612 let suggestion = DownloadedAmpSuggestion {
613 keywords: vec![
614 String::from("f"),
615 String::from("fo"),
616 String::from("foo"),
617 String::from("foo b"),
618 String::from("foo ba"),
619 String::from("foo bar"),
620 ],
621 full_keywords: vec![(String::from("foo"), 3), (String::from("foo bar"), 3)],
622 ..DownloadedAmpSuggestion::default()
623 };
624
625 assert_eq!(
626 Vec::from_iter(suggestion.keywords()),
627 vec![
628 AmpKeyword {
629 rank: 0,
630 keyword: "f",
631 full_keyword: Some("foo"),
632 },
633 AmpKeyword {
634 rank: 1,
635 keyword: "fo",
636 full_keyword: Some("foo"),
637 },
638 AmpKeyword {
639 rank: 2,
640 keyword: "foo",
641 full_keyword: Some("foo"),
642 },
643 AmpKeyword {
644 rank: 3,
645 keyword: "foo b",
646 full_keyword: Some("foo bar"),
647 },
648 AmpKeyword {
649 rank: 4,
650 keyword: "foo ba",
651 full_keyword: Some("foo bar"),
652 },
653 AmpKeyword {
654 rank: 5,
655 keyword: "foo bar",
656 full_keyword: Some("foo bar"),
657 },
658 ],
659 );
660 }
661
662 #[test]
663 fn test_missing_full_keywords() {
664 let suggestion = DownloadedAmpSuggestion {
665 keywords: vec![
666 String::from("f"),
667 String::from("fo"),
668 String::from("foo"),
669 String::from("foo b"),
670 String::from("foo ba"),
671 String::from("foo bar"),
672 ],
673 full_keywords: vec![(String::from("foo"), 3)],
675 ..DownloadedAmpSuggestion::default()
676 };
677
678 assert_eq!(
679 Vec::from_iter(suggestion.keywords()),
680 vec![
681 AmpKeyword {
682 rank: 0,
683 keyword: "f",
684 full_keyword: Some("foo"),
685 },
686 AmpKeyword {
687 rank: 1,
688 keyword: "fo",
689 full_keyword: Some("foo"),
690 },
691 AmpKeyword {
692 rank: 2,
693 keyword: "foo",
694 full_keyword: Some("foo"),
695 },
696 AmpKeyword {
697 rank: 3,
698 keyword: "foo b",
699 full_keyword: None,
700 },
701 AmpKeyword {
702 rank: 4,
703 keyword: "foo ba",
704 full_keyword: None,
705 },
706 AmpKeyword {
707 rank: 5,
708 keyword: "foo bar",
709 full_keyword: None,
710 },
711 ],
712 );
713 }
714
715 fn full_or_prefix_keywords_to_owned(
716 kws: Vec<FullOrPrefixKeywords<&str>>,
717 ) -> Vec<FullOrPrefixKeywords<String>> {
718 kws.iter()
719 .map(|val| match val {
720 FullOrPrefixKeywords::Full(s) => FullOrPrefixKeywords::Full(s.to_string()),
721 FullOrPrefixKeywords::Prefix((prefix, suffixes)) => FullOrPrefixKeywords::Prefix((
722 prefix.to_string(),
723 suffixes.iter().map(|s| s.to_string()).collect(),
724 )),
725 })
726 .collect()
727 }
728
729 #[test]
730 fn test_dynamic_keywords() {
731 let suggestion = DownloadedDynamicSuggestion {
732 keywords: full_or_prefix_keywords_to_owned(vec![
733 "no suffixes".into(),
734 ("empty suffixes", vec![]).into(),
735 ("empty string suffix", vec![""]).into(),
736 ("choco", vec!["", "bo", "late"]).into(),
737 "duplicate 1".into(),
738 "duplicate 1".into(),
739 ("dup", vec!["licate 1", "licate 2"]).into(),
740 ("dup", vec!["lo", "licate 2", "licate 3"]).into(),
741 ("duplic", vec!["ate 3", "ar", "ate 4"]).into(),
742 ("du", vec!["plicate 4", "plicate 5", "nk"]).into(),
743 ]),
744 data: None,
745 dismissal_key: None,
746 };
747
748 assert_eq!(
749 Vec::from_iter(suggestion.keywords()),
750 vec![
751 "no suffixes",
752 "empty suffixes",
753 "empty string suffix",
754 "choco",
755 "chocob",
756 "chocobo",
757 "chocol",
758 "chocola",
759 "chocolat",
760 "chocolate",
761 "duplicate 1",
762 "duplicate 1",
763 "dup",
764 "dupl",
765 "dupli",
766 "duplic",
767 "duplica",
768 "duplicat",
769 "duplicate",
770 "duplicate ",
771 "duplicate 1",
772 "dupl",
773 "dupli",
774 "duplic",
775 "duplica",
776 "duplicat",
777 "duplicate",
778 "duplicate ",
779 "duplicate 2",
780 "dup",
781 "dupl",
782 "duplo",
783 "dupl",
784 "dupli",
785 "duplic",
786 "duplica",
787 "duplicat",
788 "duplicate",
789 "duplicate ",
790 "duplicate 2",
791 "dupl",
792 "dupli",
793 "duplic",
794 "duplica",
795 "duplicat",
796 "duplicate",
797 "duplicate ",
798 "duplicate 3",
799 "duplic",
800 "duplica",
801 "duplicat",
802 "duplicate",
803 "duplicate ",
804 "duplicate 3",
805 "duplica",
806 "duplicar",
807 "duplica",
808 "duplicat",
809 "duplicate",
810 "duplicate ",
811 "duplicate 4",
812 "du",
813 "dup",
814 "dupl",
815 "dupli",
816 "duplic",
817 "duplica",
818 "duplicat",
819 "duplicate",
820 "duplicate ",
821 "duplicate 4",
822 "dup",
823 "dupl",
824 "dupli",
825 "duplic",
826 "duplica",
827 "duplicat",
828 "duplicate",
829 "duplicate ",
830 "duplicate 5",
831 "dun",
832 "dunk",
833 ],
834 );
835 }
836}