search/
sort_helpers.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//! This module defines functions for sorting search engines based on priority
6//! and order hints, falling back to alphabetical sorting when neither is provided.
7
8use crate::SearchEngineDefinition;
9
10pub(crate) fn set_engine_order(engines: &mut [SearchEngineDefinition], ordered_engines: &[String]) {
11    let mut order_number = ordered_engines.len();
12
13    for engine_id in ordered_engines {
14        if let Some(found_engine) = find_engine_with_match_mut(engines, engine_id) {
15            found_engine.order_hint = Some(order_number as u32);
16            order_number -= 1;
17        }
18    }
19}
20
21pub(crate) fn sort(
22    default_engine_id: Option<&String>,
23    default_private_engine_id: Option<&String>,
24    a: &SearchEngineDefinition,
25    b: &SearchEngineDefinition,
26) -> std::cmp::Ordering {
27    let b_index = get_priority(b, default_engine_id, default_private_engine_id);
28    let a_index = get_priority(a, default_engine_id, default_private_engine_id);
29    let order = b_index.cmp(&a_index);
30
31    // TODO: update the comparison to use ICU4X when it's available.
32    // See Bug 1945295: https://bugzilla.mozilla.org/show_bug.cgi?id=1945295
33    // If order is equal and order_hint is None for both, fall back to alphabetical sorting
34    if order == std::cmp::Ordering::Equal {
35        return a.name.cmp(&b.name);
36    }
37
38    order
39}
40
41fn find_engine_with_match_mut<'a>(
42    engines: &'a mut [SearchEngineDefinition],
43    engine_id_match: &String,
44) -> Option<&'a mut SearchEngineDefinition> {
45    if engine_id_match.is_empty() {
46        return None;
47    }
48    if let Some(match_no_star) = engine_id_match.strip_suffix('*') {
49        return engines
50            .iter_mut()
51            .find(|e| e.identifier.starts_with(match_no_star));
52    }
53
54    engines
55        .iter_mut()
56        .find(|e| e.identifier == *engine_id_match)
57}
58
59fn get_priority(
60    engine: &SearchEngineDefinition,
61    default_engine_id: Option<&String>,
62    default_private_engine_id: Option<&String>,
63) -> u32 {
64    if Some(&engine.identifier) == default_engine_id {
65        return u32::MAX;
66    }
67    if Some(&engine.identifier) == default_private_engine_id {
68        return u32::MAX - 1;
69    }
70    engine.order_hint.unwrap_or(0)
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::types::*;
77    use pretty_assertions::assert_eq;
78
79    fn create_engine(
80        engine_id: &str,
81        order_hint: Option<u32>,
82        name: Option<&str>,
83    ) -> SearchEngineDefinition {
84        SearchEngineDefinition {
85            identifier: engine_id.to_string(),
86            name: name.unwrap_or(engine_id).to_string(),
87            order_hint,
88            ..Default::default()
89        }
90    }
91
92    #[test]
93    fn test_find_engine_with_match_mut_starts_with() {
94        let mut engines = vec![
95            create_engine("wiki-ca", None, None),
96            create_engine("wiki-uk", None, None),
97            create_engine("test-engine", None, None),
98        ];
99        let found_engine = find_engine_with_match_mut(&mut engines, &"wiki*".to_string());
100
101        assert_eq!(
102            found_engine.unwrap().identifier,
103            "wiki-ca",
104            "Should match the first engine that starts with 'wiki'."
105        );
106    }
107
108    #[test]
109    fn test_set_engine_order_full_list() {
110        let mut engines = vec![
111            create_engine("last-engine", None, None),
112            create_engine("secondary-engine", None, None),
113            create_engine("primary-engine", None, None),
114        ];
115        let ordered_engines_list = vec![
116            "primary-engine".to_string(),
117            "secondary-engine".to_string(),
118            "last-engine".to_string(),
119        ];
120        set_engine_order(&mut engines, &ordered_engines_list);
121
122        let expected_order_hints = vec![
123            ("last-engine", Some(1)),
124            ("secondary-engine", Some(2)),
125            ("primary-engine", Some(3)),
126        ];
127        let actual_order_hints: Vec<(&str, Option<u32>)> = engines
128            .iter()
129            .map(|e| (e.identifier.as_str(), e.order_hint))
130            .collect();
131
132        assert_eq!(
133            actual_order_hints, expected_order_hints,
134            "Should assign correct order hints when all engines are in the ordered engines list, the first engine with the highest and decreasing for each next engine."
135        )
136    }
137
138    #[test]
139    fn test_set_engine_order_partial_list() {
140        let mut engines = vec![
141            create_engine("secondary-engine", None, None),
142            create_engine("primary-engine", None, None),
143            create_engine("no-order-hint-engine", None, None),
144        ];
145        let ordered_engines_list =
146            vec!["primary-engine".to_string(), "secondary-engine".to_string()];
147        set_engine_order(&mut engines, &ordered_engines_list);
148
149        let expected_order_hints = vec![
150            ("secondary-engine", Some(1)),
151            ("primary-engine", Some(2)),
152            ("no-order-hint-engine", None),
153        ];
154        let actual_order_hints: Vec<(&str, Option<u32>)> = engines
155            .iter()
156            .map(|e| (e.identifier.as_str(), e.order_hint))
157            .collect();
158        assert_eq!(
159            actual_order_hints, expected_order_hints,
160            "Should assign correct order hints when some of the engines are in the ordered engines list, the first engine with the highest and decreasing for each next engine."
161        )
162    }
163
164    #[test]
165    fn test_sort_engines_by_order_hint() {
166        let default_engine_id = None;
167        let default_private_engine_id = None;
168        let mut engines = vec![
169            create_engine("c-engine", Some(3), None),
170            create_engine("b-engine", Some(2), None),
171            create_engine("a-engine", Some(1), None),
172        ];
173        engines.sort_by(|a, b| {
174            sort(
175                default_engine_id.as_ref(),
176                default_private_engine_id.as_ref(),
177                a,
178                b,
179            )
180        });
181
182        let actual_order: Vec<&str> = engines.iter().map(|e| e.identifier.as_str()).collect();
183        let expected_order = vec!["c-engine", "b-engine", "a-engine"];
184        assert_eq!(
185            actual_order, expected_order,
186            "Should sort engines by descending order hint, with the highest order hint appearing first."
187        )
188    }
189
190    #[test]
191    fn test_sort_engines_alphabetically_without_order_hint() {
192        let default_engine_id = None;
193        let default_private_engine_id = None;
194        let mut engines = vec![
195            create_engine("c-engine", None, None),
196            create_engine("b-engine", None, None),
197            create_engine("a-engine", None, None),
198        ];
199        engines.sort_by(|a, b| {
200            sort(
201                default_engine_id.as_ref(),
202                default_private_engine_id.as_ref(),
203                a,
204                b,
205            )
206        });
207
208        let actual_order: Vec<&str> = engines.iter().map(|e| e.identifier.as_str()).collect();
209        let expected_order = vec!["a-engine", "b-engine", "c-engine"];
210        assert_eq!(
211            actual_order, expected_order,
212            "Should sort engines alphabetically when there are no order hints."
213        )
214    }
215
216    #[test]
217    fn test_sort_engines_by_order_hint_and_alphabetically() {
218        let default_engine_id = None;
219        let default_private_engine_id = None;
220        let mut engines = vec![
221            // Identifiers are the opposite order to the names, so that we
222            // can show that we are sorting alphabetically by name.
223            create_engine("d-engine", None, Some("Charlie")),
224            create_engine("e-engine", None, Some("Beta")),
225            create_engine("f-engine", None, Some("Alpha")),
226            create_engine("c-engine", Some(4), None),
227            create_engine("b-engine", Some(5), None),
228            create_engine("a-engine", Some(6), None),
229        ];
230        engines.sort_by(|a, b| {
231            sort(
232                default_engine_id.as_ref(),
233                default_private_engine_id.as_ref(),
234                a,
235                b,
236            )
237        });
238
239        let actual_order: Vec<&str> = engines.iter().map(|e| e.identifier.as_str()).collect();
240        let expected_order = vec![
241            "a-engine", "b-engine", "c-engine", "f-engine", "e-engine", "d-engine",
242        ];
243        assert_eq!(
244            actual_order, expected_order,
245            "Should sort engines by order hint before sorting alphabetically."
246        )
247    }
248
249    #[test]
250    fn test_sort_engines_with_defaults() {
251        let default_engine_id = Some("a-engine".to_string());
252        let default_private_engine_id = Some("b-engine".to_string());
253        let mut engines = vec![
254            create_engine("c-engine", Some(3), None),
255            create_engine("a-engine", Some(1), None), // Default engine should be first
256            create_engine("b-engine", Some(2), None), // Default private engine should be second
257        ];
258        engines.sort_by(|a, b| {
259            sort(
260                default_engine_id.as_ref(),
261                default_private_engine_id.as_ref(),
262                a,
263                b,
264            )
265        });
266
267        let actual_order: Vec<&str> = engines.iter().map(|e| e.identifier.as_str()).collect();
268        let expected_order = vec!["a-engine", "b-engine", "c-engine"];
269        assert_eq!(
270            actual_order, expected_order,
271            "Should have sorted the default and private default to have the highest priority."
272        )
273    }
274
275    #[test]
276    fn test_sort_engines_non_ascii_without_order_hint() {
277        // TODO: update the comparison to use ICU4X when it's available.
278        // See Bug 1945295: https://bugzilla.mozilla.org/show_bug.cgi?id=1945295
279    }
280}