1use 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
24pub(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#[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 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 9 => Some(Self::Dynamic),
98 _ => None,
99 }
100 }
101
102 pub(crate) fn primary_collection(&self) -> Collection {
104 match self {
105 Self::Amp => Collection::Amp,
106 _ => Collection::Other,
107 }
108 }
109
110 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 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 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#[derive(Clone, Default, Debug, uniffi::Record)]
211pub struct SuggestionProviderConstraints {
212 #[uniffi(default = None)]
215 pub dynamic_suggestion_types: Option<Vec<String>>,
216 #[uniffi(default = None)]
219 pub amp_alternative_matching: Option<AmpMatchingStrategy>,
220}
221
222#[derive(Clone, Debug, uniffi::Enum)]
223pub enum AmpMatchingStrategy {
224 NoKeywordExpansion = 1, FtsAgainstFullKeywords,
230 FtsAgainstTitle,
232}
233
234impl AmpMatchingStrategy {
235 pub fn uses_fts(&self) -> bool {
236 matches!(self, Self::FtsAgainstFullKeywords | Self::FtsAgainstTitle)
237 }
238}