1use chrono::Local;
7
8use crate::{db::DEFAULT_SUGGESTION_SCORE, geoname::Geoname};
9
10const TIMESTAMP_TEMPLATE: &str = "%YYYYMMDDHH%";
12
13const TIMESTAMP_LENGTH: usize = 10;
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, uniffi::Enum)]
21#[repr(u8)]
22pub enum YelpSubjectType {
23 Service = 0,
25 Business = 1,
27}
28
29#[derive(Clone, Debug, PartialEq, uniffi::Enum)]
31pub enum Suggestion {
32 Amp {
33 title: String,
34 url: String,
35 raw_url: String,
36 icon: Option<Vec<u8>>,
37 icon_mimetype: Option<String>,
38 full_keyword: String,
39 block_id: i64,
40 advertiser: String,
41 iab_category: String,
42 categories: Vec<i32>,
43 impression_url: String,
44 click_url: String,
45 raw_click_url: String,
46 score: f64,
47 fts_match_info: Option<FtsMatchInfo>,
48 },
49 Wikipedia {
50 title: String,
51 url: String,
52 icon: Option<Vec<u8>>,
53 icon_mimetype: Option<String>,
54 full_keyword: String,
55 },
56 Amo {
57 title: String,
58 url: String,
59 icon_url: String,
60 description: String,
61 rating: Option<String>,
62 number_of_ratings: i64,
63 guid: String,
64 score: f64,
65 },
66 Yelp {
67 url: String,
68 title: String,
69 icon: Option<Vec<u8>>,
70 icon_mimetype: Option<String>,
71 score: f64,
72 has_location_sign: bool,
73 subject_exact_match: bool,
74 subject_type: YelpSubjectType,
75 location_param: String,
76 },
77 Mdn {
78 title: String,
79 url: String,
80 description: String,
81 score: f64,
82 },
83 Weather {
84 city: Option<Geoname>,
85 score: f64,
86 },
87 Dynamic {
88 suggestion_type: String,
89 data: Option<serde_json::Value>,
90 dismissal_key: Option<String>,
95 score: f64,
96 },
97}
98
99#[derive(Debug, Clone, PartialEq, uniffi::Record)]
101pub struct FtsMatchInfo {
102 pub prefix: bool,
104 pub stemming: bool,
106}
107
108impl PartialOrd for Suggestion {
109 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
110 Some(self.cmp(other))
111 }
112}
113
114impl Ord for Suggestion {
115 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
116 other
117 .score()
118 .partial_cmp(&self.score())
119 .unwrap_or(std::cmp::Ordering::Equal)
120 }
121}
122
123impl Suggestion {
124 pub fn dismissal_key(&self) -> Option<&str> {
128 match self {
129 Self::Amp { full_keyword, .. } => {
130 if !full_keyword.is_empty() {
131 Some(full_keyword)
132 } else {
133 self.raw_url()
134 }
135 }
136 Self::Dynamic { dismissal_key, .. } => dismissal_key.as_deref(),
137 Self::Wikipedia { .. }
138 | Self::Amo { .. }
139 | Self::Yelp { .. }
140 | Self::Mdn { .. }
141 | Self::Weather { .. } => self.raw_url(),
142 }
143 }
144
145 pub fn url(&self) -> Option<&str> {
147 match self {
148 Self::Amp { url, .. }
149 | Self::Wikipedia { url, .. }
150 | Self::Amo { url, .. }
151 | Self::Yelp { url, .. }
152 | Self::Mdn { url, .. } => Some(url),
153 Self::Weather { .. } | Self::Dynamic { .. } => None,
154 }
155 }
156
157 pub fn raw_url(&self) -> Option<&str> {
162 match self {
163 Self::Amp { raw_url, .. } => Some(raw_url),
164 Self::Wikipedia { .. }
165 | Self::Amo { .. }
166 | Self::Yelp { .. }
167 | Self::Mdn { .. }
168 | Self::Weather { .. }
169 | Self::Dynamic { .. } => self.url(),
170 }
171 }
172
173 pub fn title(&self) -> &str {
174 match self {
175 Self::Amp { title, .. }
176 | Self::Wikipedia { title, .. }
177 | Self::Amo { title, .. }
178 | Self::Yelp { title, .. }
179 | Self::Mdn { title, .. } => title,
180 _ => "untitled",
181 }
182 }
183
184 pub fn icon_data(&self) -> Option<&[u8]> {
185 match self {
186 Self::Amp { icon, .. } | Self::Wikipedia { icon, .. } | Self::Yelp { icon, .. } => {
187 icon.as_deref()
188 }
189 _ => None,
190 }
191 }
192
193 pub fn score(&self) -> f64 {
194 match self {
195 Self::Amp { score, .. }
196 | Self::Amo { score, .. }
197 | Self::Yelp { score, .. }
198 | Self::Mdn { score, .. }
199 | Self::Weather { score, .. }
200 | Self::Dynamic { score, .. } => *score,
201 Self::Wikipedia { .. } => DEFAULT_SUGGESTION_SCORE,
202 }
203 }
204
205 pub fn fts_match_info(&self) -> Option<&FtsMatchInfo> {
206 None
207 }
208}
209
210impl Eq for Suggestion {}
211pub(crate) fn cook_raw_suggestion_url(raw_url: &str) -> String {
214 let timestamp = Local::now().format("%Y%m%d%H").to_string();
215 debug_assert!(timestamp.len() == TIMESTAMP_LENGTH);
216 raw_url.replacen(TIMESTAMP_TEMPLATE, ×tamp, 1)
219}
220
221#[uniffi::export]
225pub fn raw_suggestion_url_matches(raw_url: &str, cooked_url: &str) -> bool {
226 let Some((raw_url_prefix, raw_url_suffix)) = raw_url.split_once(TIMESTAMP_TEMPLATE) else {
227 return raw_url == cooked_url;
228 };
229 let (Some(cooked_url_prefix), Some(cooked_url_suffix)) = (
230 cooked_url.get(..raw_url_prefix.len()),
231 cooked_url.get(raw_url_prefix.len() + TIMESTAMP_LENGTH..),
232 ) else {
233 return false;
234 };
235 if raw_url_prefix != cooked_url_prefix || raw_url_suffix != cooked_url_suffix {
236 return false;
237 }
238 let maybe_timestamp =
239 &cooked_url[raw_url_prefix.len()..raw_url_prefix.len() + TIMESTAMP_LENGTH];
240 maybe_timestamp.bytes().all(|b| b.is_ascii_digit())
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn cook_url_with_template_parameters() {
249 let raw_url_with_one_timestamp = "https://example.com?a=%YYYYMMDDHH%";
250 let cooked_url_with_one_timestamp = cook_raw_suggestion_url(raw_url_with_one_timestamp);
251 assert_eq!(
252 cooked_url_with_one_timestamp.len(),
253 raw_url_with_one_timestamp.len() - 2
254 );
255 assert_ne!(raw_url_with_one_timestamp, cooked_url_with_one_timestamp);
256
257 let raw_url_with_trailing_segment = "https://example.com?a=%YYYYMMDDHH%&b=c";
258 let cooked_url_with_trailing_segment =
259 cook_raw_suggestion_url(raw_url_with_trailing_segment);
260 assert_eq!(
261 cooked_url_with_trailing_segment.len(),
262 raw_url_with_trailing_segment.len() - 2
263 );
264 assert_ne!(
265 raw_url_with_trailing_segment,
266 cooked_url_with_trailing_segment
267 );
268 }
269
270 #[test]
271 fn cook_url_without_template_parameters() {
272 let raw_url_without_timestamp = "https://example.com?b=c";
273 let cooked_url_without_timestamp = cook_raw_suggestion_url(raw_url_without_timestamp);
274 assert_eq!(raw_url_without_timestamp, cooked_url_without_timestamp);
275 }
276
277 #[test]
278 fn url_with_template_parameters_matches() {
279 let raw_url_with_one_timestamp = "https://example.com?a=%YYYYMMDDHH%";
280 let raw_url_with_trailing_segment = "https://example.com?a=%YYYYMMDDHH%&b=c";
281
282 assert!(raw_suggestion_url_matches(
284 raw_url_with_one_timestamp,
285 "https://example.com?a=0000000000"
286 ));
287 assert!(raw_suggestion_url_matches(
288 raw_url_with_trailing_segment,
289 "https://example.com?a=1111111111&b=c"
290 ));
291
292 assert!(!raw_suggestion_url_matches(
294 raw_url_with_one_timestamp,
295 "https://example.com?a=1234567890&c=d"
296 ));
297 assert!(!raw_suggestion_url_matches(
298 raw_url_with_one_timestamp,
299 "https://example.com?a=123456789"
300 ));
301 assert!(!raw_suggestion_url_matches(
302 raw_url_with_trailing_segment,
303 "https://example.com?a=0987654321"
304 ));
305 assert!(!raw_suggestion_url_matches(
306 raw_url_with_trailing_segment,
307 "https://example.com?a=0987654321&b=c&d=e"
308 ));
309
310 assert!(!raw_suggestion_url_matches(
312 raw_url_with_one_timestamp, "https://example.com?b=4444444444" ));
315 assert!(!raw_suggestion_url_matches(
316 raw_url_with_trailing_segment, "https://example.com?a=5555555555&c=c" ));
319
320 assert!(!raw_suggestion_url_matches(
322 raw_url_with_one_timestamp,
323 "https://example.com?a=bcdefghijk"
324 ));
325 assert!(!raw_suggestion_url_matches(
326 raw_url_with_trailing_segment,
327 "https://example.com?a=bcdefghijk&b=c"
328 ));
329 }
330
331 #[test]
332 fn url_without_template_parameters_matches() {
333 let raw_url_without_timestamp = "https://example.com?b=c";
334
335 assert!(raw_suggestion_url_matches(
336 raw_url_without_timestamp,
337 "https://example.com?b=c"
338 ));
339 assert!(!raw_suggestion_url_matches(
340 raw_url_without_timestamp,
341 "http://example.com"
342 ));
343 assert!(!raw_suggestion_url_matches(
344 raw_url_without_timestamp, "http://example.com?a=c" ));
347 assert!(!raw_suggestion_url_matches(
348 raw_url_without_timestamp,
349 "https://example.com?b=c&d=e"
350 ));
351 }
352}