suggest/
provider.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 */
5
6use std::{
7    collections::{HashMap, HashSet},
8    fmt,
9};
10
11use rusqlite::{
12    types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef},
13    Result as RusqliteResult,
14};
15
16use crate::rs::{Collection, SuggestRecordType};
17
18#[cfg(test)]
19use serde_json::Value as JsonValue;
20
21#[cfg(test)]
22use crate::testing::{MockAttachment, MockIcon, MockRecord};
23
24/// Record types from these providers will be ingested when consumers do not
25/// specify providers in `SuggestIngestionConstraints`.
26pub(crate) const DEFAULT_INGEST_PROVIDERS: [SuggestionProvider; 5] = [
27    SuggestionProvider::Amp,
28    SuggestionProvider::Wikipedia,
29    SuggestionProvider::Amo,
30    SuggestionProvider::Yelp,
31    SuggestionProvider::Mdn,
32];
33
34/// A provider is a source of search suggestions.
35/// Please preserve the integer values after removing or adding providers.
36/// Provider configs are associated with integer keys stored in the database.
37#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, uniffi::Enum)]
38#[repr(u8)]
39pub enum SuggestionProvider {
40    Amp = 1,
41    Wikipedia = 2,
42    Amo = 3,
43    Yelp = 5,
44    Mdn = 6,
45    Weather = 7,
46    // Fakespot = 8, removed
47    Dynamic = 9,
48}
49
50impl fmt::Display for SuggestionProvider {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::Amp => write!(f, "amp"),
54            Self::Wikipedia => write!(f, "wikipedia"),
55            Self::Amo => write!(f, "amo"),
56            Self::Yelp => write!(f, "yelp"),
57            Self::Mdn => write!(f, "mdn"),
58            Self::Weather => write!(f, "weather"),
59            Self::Dynamic => write!(f, "dynamic"),
60        }
61    }
62}
63
64impl FromSql for SuggestionProvider {
65    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
66        let v = value.as_i64()?;
67        u8::try_from(v)
68            .ok()
69            .and_then(SuggestionProvider::from_u8)
70            .ok_or_else(|| FromSqlError::OutOfRange(v))
71    }
72}
73
74impl SuggestionProvider {
75    pub fn all() -> [Self; 7] {
76        [
77            Self::Amp,
78            Self::Wikipedia,
79            Self::Amo,
80            Self::Yelp,
81            Self::Mdn,
82            Self::Weather,
83            Self::Dynamic,
84        ]
85    }
86
87    #[inline]
88    pub(crate) fn from_u8(v: u8) -> Option<Self> {
89        match v {
90            1 => Some(Self::Amp),
91            2 => Some(Self::Wikipedia),
92            3 => Some(Self::Amo),
93            5 => Some(Self::Yelp),
94            6 => Some(Self::Mdn),
95            7 => Some(Self::Weather),
96            // 8 => Some(Self::Fakespot), removed
97            9 => Some(Self::Dynamic),
98            _ => None,
99        }
100    }
101
102    /// The collection that stores the provider's primary record.
103    pub(crate) fn primary_collection(&self) -> Collection {
104        match self {
105            Self::Amp => Collection::Amp,
106            _ => Collection::Other,
107        }
108    }
109
110    /// The provider's primary record type.
111    pub(crate) fn primary_record_type(&self) -> SuggestRecordType {
112        match self {
113            Self::Amp => SuggestRecordType::Amp,
114            Self::Wikipedia => SuggestRecordType::Wikipedia,
115            Self::Amo => SuggestRecordType::Amo,
116            Self::Yelp => SuggestRecordType::Yelp,
117            Self::Mdn => SuggestRecordType::Mdn,
118            Self::Weather => SuggestRecordType::Weather,
119            Self::Dynamic => SuggestRecordType::Dynamic,
120        }
121    }
122
123    /// Other record types and their collections that the provider depends on.
124    fn secondary_record_types(&self) -> Option<HashMap<Collection, HashSet<SuggestRecordType>>> {
125        match self {
126            Self::Amp => Some(HashMap::from([(
127                Collection::Amp,
128                HashSet::from([SuggestRecordType::Icon]),
129            )])),
130            Self::Wikipedia => Some(HashMap::from([(
131                Collection::Other,
132                HashSet::from([SuggestRecordType::Icon]),
133            )])),
134            Self::Yelp => Some(HashMap::from([(
135                Collection::Other,
136                HashSet::from([
137                    SuggestRecordType::Icon,
138                    SuggestRecordType::Geonames,
139                    SuggestRecordType::GeonamesAlternates,
140                ]),
141            )])),
142            Self::Weather => Some(HashMap::from([(
143                Collection::Other,
144                HashSet::from([
145                    SuggestRecordType::Geonames,
146                    SuggestRecordType::GeonamesAlternates,
147                ]),
148            )])),
149            _ => None,
150        }
151    }
152
153    /// All record types and their collections that the provider depends on,
154    /// including primary and secondary records.
155    pub(crate) fn record_types_by_collection(
156        &self,
157    ) -> HashMap<Collection, HashSet<SuggestRecordType>> {
158        let mut rts = self.secondary_record_types().unwrap_or_default();
159        rts.entry(self.primary_collection())
160            .or_default()
161            .insert(self.primary_record_type());
162        rts
163    }
164}
165
166impl ToSql for SuggestionProvider {
167    fn to_sql(&self) -> RusqliteResult<ToSqlOutput<'_>> {
168        Ok(ToSqlOutput::from(*self as u8))
169    }
170}
171
172#[cfg(test)]
173impl SuggestionProvider {
174    pub fn record(&self, record_id: &str, attachment: JsonValue) -> MockRecord {
175        self.full_record(record_id, None, Some(MockAttachment::Json(attachment)))
176    }
177
178    pub fn empty_record(&self, record_id: &str) -> MockRecord {
179        self.full_record(record_id, None, None)
180    }
181
182    pub fn full_record(
183        &self,
184        record_id: &str,
185        inline_data: Option<JsonValue>,
186        attachment: Option<MockAttachment>,
187    ) -> MockRecord {
188        MockRecord {
189            collection: self.primary_collection(),
190            record_type: self.primary_record_type(),
191            id: record_id.to_string(),
192            inline_data,
193            attachment,
194        }
195    }
196
197    pub fn icon(&self, icon: MockIcon) -> MockRecord {
198        MockRecord {
199            collection: self.primary_collection(),
200            record_type: SuggestRecordType::Icon,
201            id: format!("icon-{}", icon.id),
202            inline_data: None,
203            attachment: Some(MockAttachment::Icon(icon)),
204        }
205    }
206}
207
208/// Some providers manage multiple suggestion subtypes. Queries, ingests, and
209/// other operations on those providers must be constrained to a desired subtype.
210#[derive(Clone, Default, Debug, uniffi::Record)]
211pub struct SuggestionProviderConstraints {
212    /// Which dynamic suggestions should we fetch or ingest? Corresponds to the
213    /// `suggestion_type` value in dynamic suggestions remote settings records.
214    #[uniffi(default = None)]
215    pub dynamic_suggestion_types: Option<Vec<String>>,
216    /// Which strategy should we use for the AMP queries?
217    /// Use None for the default strategy.
218    #[uniffi(default = None)]
219    pub amp_alternative_matching: Option<AmpMatchingStrategy>,
220}
221
222#[derive(Clone, Debug, uniffi::Enum)]
223pub enum AmpMatchingStrategy {
224    /// Disable keywords added via keyword expansion.
225    /// This eliminates keywords that for terms related to the "real" keywords, for example
226    /// misspellings like "underarmor" instead of "under armor"'.
227    NoKeywordExpansion = 1, // The desktop consumer assumes this starts at `1`
228    /// Use FTS matching against the full keywords, joined together.
229    FtsAgainstFullKeywords,
230    /// Use FTS matching against the title field
231    FtsAgainstTitle,
232}
233
234impl AmpMatchingStrategy {
235    pub fn uses_fts(&self) -> bool {
236        matches!(self, Self::FtsAgainstFullKeywords | Self::FtsAgainstTitle)
237    }
238}