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 serp_categories: Option<Vec<i32>>,
374 pub click_url: String,
375 pub impression_url: String,
376 #[serde(rename = "icon")]
377 pub icon_id: String,
378}
379
380#[derive(Clone, Debug, Default, Deserialize)]
382pub(crate) struct DownloadedWikipediaSuggestion {
383 pub keywords: Vec<String>,
384 pub title: String,
385 pub url: String,
386 pub score: Option<f64>,
387 #[serde(default)]
388 pub full_keywords: Vec<(String, usize)>,
389 #[serde(rename = "icon")]
390 pub icon_id: String,
391}
392
393pub fn iterate_keywords<'a>(
395 keywords: &'a [String],
396 full_keywords: &'a [(String, usize)],
397) -> impl Iterator<Item = AmpKeyword<'a>> {
398 let full_keywords_iter = full_keywords
399 .iter()
400 .flat_map(|(full_keyword, repeat_for)| {
401 std::iter::repeat_n(Some(full_keyword.as_str()), *repeat_for)
402 })
403 .chain(std::iter::repeat(None)); keywords
406 .iter()
407 .zip(full_keywords_iter)
408 .enumerate()
409 .map(move |(i, (keyword, full_keyword))| AmpKeyword {
410 rank: i,
411 keyword,
412 full_keyword,
413 })
414}
415
416impl DownloadedAmpSuggestion {
417 pub fn keywords(&self) -> impl Iterator<Item = AmpKeyword<'_>> {
418 iterate_keywords(&self.keywords, &self.full_keywords)
419 }
420
421 pub fn full_keywords_fts_column(&self) -> String {
422 full_keywords_to_fts_content(self.full_keywords.iter().map(|(s, _)| s.as_str()))
423 }
424}
425
426impl DownloadedWikipediaSuggestion {
427 pub fn keywords(&self) -> impl Iterator<Item = AmpKeyword<'_>> {
428 iterate_keywords(&self.keywords, &self.full_keywords)
429 }
430}
431
432#[derive(Debug, PartialEq, Eq)]
433pub(crate) struct AmpKeyword<'a> {
434 pub rank: usize,
435 pub keyword: &'a str,
436 pub full_keyword: Option<&'a str>,
437}
438
439#[derive(Clone, Debug, Deserialize)]
441pub(crate) struct DownloadedAmoSuggestion {
442 pub description: String,
443 pub url: String,
444 pub guid: String,
445 #[serde(rename = "icon")]
446 pub icon_url: String,
447 pub rating: Option<String>,
448 pub number_of_ratings: i64,
449 pub title: String,
450 pub keywords: Vec<String>,
451 pub score: f64,
452}
453#[derive(Clone, Debug, Deserialize)]
455#[serde(untagged)]
456pub enum DownloadedYelpLocationSign {
457 V1 { keyword: String },
458 V2(String),
459}
460impl ToSql for DownloadedYelpLocationSign {
461 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
462 let keyword = match self {
463 DownloadedYelpLocationSign::V1 { keyword } => keyword,
464 DownloadedYelpLocationSign::V2(keyword) => keyword,
465 };
466 Ok(ToSqlOutput::from(keyword.as_str()))
467 }
468}
469#[derive(Clone, Debug, Deserialize)]
471pub(crate) struct DownloadedYelpSuggestion {
472 pub subjects: Vec<String>,
473 #[serde(rename = "businessSubjects")]
474 pub business_subjects: Option<Vec<String>>,
475 #[serde(rename = "preModifiers")]
476 pub pre_modifiers: Vec<String>,
477 #[serde(rename = "postModifiers")]
478 pub post_modifiers: Vec<String>,
479 #[serde(rename = "locationSigns")]
480 pub location_signs: Vec<DownloadedYelpLocationSign>,
481 #[serde(rename = "yelpModifiers")]
482 pub yelp_modifiers: Vec<String>,
483 #[serde(rename = "icon")]
484 pub icon_id: String,
485 pub score: f64,
486}
487
488#[derive(Clone, Debug, Deserialize)]
490pub(crate) struct DownloadedMdnSuggestion {
491 pub url: String,
492 pub title: String,
493 pub description: String,
494 pub keywords: Vec<String>,
495 pub score: f64,
496}
497
498#[derive(Clone, Debug, Deserialize)]
500pub(crate) struct DownloadedFakespotSuggestion {
501 pub fakespot_grade: String,
502 pub product_id: String,
503 pub keywords: String,
504 pub product_type: String,
505 pub rating: f64,
506 pub score: f64,
507 pub title: String,
508 pub total_reviews: i64,
509 pub url: String,
510}
511
512#[derive(Clone, Debug, Deserialize, Serialize)]
514pub(crate) struct DownloadedDynamicRecord {
515 pub suggestion_type: String,
516 pub score: Option<f64>,
517}
518
519#[derive(Clone, Debug, Deserialize)]
521pub(crate) struct DownloadedDynamicSuggestion {
522 keywords: Vec<FullOrPrefixKeywords<String>>,
523 pub dismissal_key: Option<String>,
524 pub data: Option<Value>,
525}
526
527impl DownloadedDynamicSuggestion {
528 pub fn keywords(&self) -> impl Iterator<Item = String> + '_ {
535 self.keywords.iter().flat_map(|e| e.keywords())
536 }
537}
538
539#[derive(Clone, Debug, Deserialize)]
560#[serde(untagged)]
561enum FullOrPrefixKeywords<T> {
562 Full(T),
563 Prefix((T, Vec<T>)),
564}
565
566impl<T> From<T> for FullOrPrefixKeywords<T> {
567 fn from(full_keyword: T) -> Self {
568 Self::Full(full_keyword)
569 }
570}
571
572impl<T> From<(T, Vec<T>)> for FullOrPrefixKeywords<T> {
573 fn from(prefix_suffixes: (T, Vec<T>)) -> Self {
574 Self::Prefix(prefix_suffixes)
575 }
576}
577
578impl FullOrPrefixKeywords<String> {
579 pub fn keywords(&self) -> Box<dyn Iterator<Item = String> + '_> {
580 match self {
581 FullOrPrefixKeywords::Full(kw) => Box::new(std::iter::once(kw.to_owned())),
582 FullOrPrefixKeywords::Prefix((prefix, suffixes)) => Box::new(
583 std::iter::once(prefix.to_owned()).chain(suffixes.iter().flat_map(|suffix| {
584 let mut kw = prefix.clone();
585 suffix.chars().map(move |c| {
586 kw.push(c);
587 kw.clone()
588 })
589 })),
590 ),
591 }
592 }
593}
594
595#[derive(Clone, Debug, Deserialize, Serialize)]
597pub(crate) struct DownloadedGlobalConfig {
598 pub configuration: DownloadedGlobalConfigInner,
599}
600#[derive(Clone, Debug, Deserialize, Serialize)]
601pub(crate) struct DownloadedGlobalConfigInner {
602 pub show_less_frequently_cap: i32,
605}
606
607#[cfg(test)]
608mod test {
609 use super::*;
610
611 #[test]
612 fn test_full_keywords() {
613 let suggestion = DownloadedAmpSuggestion {
614 keywords: vec![
615 String::from("f"),
616 String::from("fo"),
617 String::from("foo"),
618 String::from("foo b"),
619 String::from("foo ba"),
620 String::from("foo bar"),
621 ],
622 full_keywords: vec![(String::from("foo"), 3), (String::from("foo bar"), 3)],
623 ..DownloadedAmpSuggestion::default()
624 };
625
626 assert_eq!(
627 Vec::from_iter(suggestion.keywords()),
628 vec![
629 AmpKeyword {
630 rank: 0,
631 keyword: "f",
632 full_keyword: Some("foo"),
633 },
634 AmpKeyword {
635 rank: 1,
636 keyword: "fo",
637 full_keyword: Some("foo"),
638 },
639 AmpKeyword {
640 rank: 2,
641 keyword: "foo",
642 full_keyword: Some("foo"),
643 },
644 AmpKeyword {
645 rank: 3,
646 keyword: "foo b",
647 full_keyword: Some("foo bar"),
648 },
649 AmpKeyword {
650 rank: 4,
651 keyword: "foo ba",
652 full_keyword: Some("foo bar"),
653 },
654 AmpKeyword {
655 rank: 5,
656 keyword: "foo bar",
657 full_keyword: Some("foo bar"),
658 },
659 ],
660 );
661 }
662
663 #[test]
664 fn test_missing_full_keywords() {
665 let suggestion = DownloadedAmpSuggestion {
666 keywords: vec![
667 String::from("f"),
668 String::from("fo"),
669 String::from("foo"),
670 String::from("foo b"),
671 String::from("foo ba"),
672 String::from("foo bar"),
673 ],
674 full_keywords: vec![(String::from("foo"), 3)],
676 ..DownloadedAmpSuggestion::default()
677 };
678
679 assert_eq!(
680 Vec::from_iter(suggestion.keywords()),
681 vec![
682 AmpKeyword {
683 rank: 0,
684 keyword: "f",
685 full_keyword: Some("foo"),
686 },
687 AmpKeyword {
688 rank: 1,
689 keyword: "fo",
690 full_keyword: Some("foo"),
691 },
692 AmpKeyword {
693 rank: 2,
694 keyword: "foo",
695 full_keyword: Some("foo"),
696 },
697 AmpKeyword {
698 rank: 3,
699 keyword: "foo b",
700 full_keyword: None,
701 },
702 AmpKeyword {
703 rank: 4,
704 keyword: "foo ba",
705 full_keyword: None,
706 },
707 AmpKeyword {
708 rank: 5,
709 keyword: "foo bar",
710 full_keyword: None,
711 },
712 ],
713 );
714 }
715
716 fn full_or_prefix_keywords_to_owned(
717 kws: Vec<FullOrPrefixKeywords<&str>>,
718 ) -> Vec<FullOrPrefixKeywords<String>> {
719 kws.iter()
720 .map(|val| match val {
721 FullOrPrefixKeywords::Full(s) => FullOrPrefixKeywords::Full(s.to_string()),
722 FullOrPrefixKeywords::Prefix((prefix, suffixes)) => FullOrPrefixKeywords::Prefix((
723 prefix.to_string(),
724 suffixes.iter().map(|s| s.to_string()).collect(),
725 )),
726 })
727 .collect()
728 }
729
730 #[test]
731 fn test_dynamic_keywords() {
732 let suggestion = DownloadedDynamicSuggestion {
733 keywords: full_or_prefix_keywords_to_owned(vec![
734 "no suffixes".into(),
735 ("empty suffixes", vec![]).into(),
736 ("empty string suffix", vec![""]).into(),
737 ("choco", vec!["", "bo", "late"]).into(),
738 "duplicate 1".into(),
739 "duplicate 1".into(),
740 ("dup", vec!["licate 1", "licate 2"]).into(),
741 ("dup", vec!["lo", "licate 2", "licate 3"]).into(),
742 ("duplic", vec!["ate 3", "ar", "ate 4"]).into(),
743 ("du", vec!["plicate 4", "plicate 5", "nk"]).into(),
744 ]),
745 data: None,
746 dismissal_key: None,
747 };
748
749 assert_eq!(
750 Vec::from_iter(suggestion.keywords()),
751 vec![
752 "no suffixes",
753 "empty suffixes",
754 "empty string suffix",
755 "choco",
756 "chocob",
757 "chocobo",
758 "chocol",
759 "chocola",
760 "chocolat",
761 "chocolate",
762 "duplicate 1",
763 "duplicate 1",
764 "dup",
765 "dupl",
766 "dupli",
767 "duplic",
768 "duplica",
769 "duplicat",
770 "duplicate",
771 "duplicate ",
772 "duplicate 1",
773 "dupl",
774 "dupli",
775 "duplic",
776 "duplica",
777 "duplicat",
778 "duplicate",
779 "duplicate ",
780 "duplicate 2",
781 "dup",
782 "dupl",
783 "duplo",
784 "dupl",
785 "dupli",
786 "duplic",
787 "duplica",
788 "duplicat",
789 "duplicate",
790 "duplicate ",
791 "duplicate 2",
792 "dupl",
793 "dupli",
794 "duplic",
795 "duplica",
796 "duplicat",
797 "duplicate",
798 "duplicate ",
799 "duplicate 3",
800 "duplic",
801 "duplica",
802 "duplicat",
803 "duplicate",
804 "duplicate ",
805 "duplicate 3",
806 "duplica",
807 "duplicar",
808 "duplica",
809 "duplicat",
810 "duplicate",
811 "duplicate ",
812 "duplicate 4",
813 "du",
814 "dup",
815 "dupl",
816 "dupli",
817 "duplic",
818 "duplica",
819 "duplicat",
820 "duplicate",
821 "duplicate ",
822 "duplicate 4",
823 "dup",
824 "dupl",
825 "dupli",
826 "duplic",
827 "duplica",
828 "duplicat",
829 "duplicate",
830 "duplicate ",
831 "duplicate 5",
832 "dun",
833 "dunk",
834 ],
835 );
836 }
837}