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