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
78    fn create_engine(
79        engine_id: &str,
80        order_hint: Option<u32>,
81        name: Option<&str>,
82    ) -> SearchEngineDefinition {
83        SearchEngineDefinition {
84            identifier: engine_id.to_string(),
85            name: name.unwrap_or(engine_id).to_string(),
86            order_hint,
87            ..Default::default()
88        }
89    }
90
91    #[test]
92    fn test_find_engine_with_match_mut_starts_with() {
93        let mut engines = vec![
94            create_engine("wiki-ca", None, None),
95            create_engine("wiki-uk", None, None),
96            create_engine("test-engine", None, None),
97        ];
98        let found_engine = find_engine_with_match_mut(&mut engines, &"wiki*".to_string());
99
100        assert_eq!(
101            found_engine.unwrap().identifier,
102            "wiki-ca",
103            "Should match the first engine that starts with 'wiki'."
104        );
105    }
106
107    #[test]
108    fn test_set_engine_order_full_list() {
109        let mut engines = vec![
110            create_engine("last-engine", None, None),
111            create_engine("secondary-engine", None, None),
112            create_engine("primary-engine", None, None),
113        ];
114        let ordered_engines_list = vec![
115            "primary-engine".to_string(),
116            "secondary-engine".to_string(),
117            "last-engine".to_string(),
118        ];
119        set_engine_order(&mut engines, &ordered_engines_list);
120
121        let expected_order_hints = vec![
122            ("last-engine", Some(1)),
123            ("secondary-engine", Some(2)),
124            ("primary-engine", Some(3)),
125        ];
126        let actual_order_hints: Vec<(&str, Option<u32>)> = engines
127            .iter()
128            .map(|e| (e.identifier.as_str(), e.order_hint))
129            .collect();
130
131        assert_eq!(
132            actual_order_hints, expected_order_hints,
133            "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."
134        )
135    }
136
137    #[test]
138    fn test_set_engine_order_partial_list() {
139        let mut engines = vec![
140            create_engine("secondary-engine", None, None),
141            create_engine("primary-engine", None, None),
142            create_engine("no-order-hint-engine", None, None),
143        ];
144        let ordered_engines_list =
145            vec!["primary-engine".to_string(), "secondary-engine".to_string()];
146        set_engine_order(&mut engines, &ordered_engines_list);
147
148        let expected_order_hints = vec![
149            ("secondary-engine", Some(1)),
150            ("primary-engine", Some(2)),
151            ("no-order-hint-engine", None),
152        ];
153        let actual_order_hints: Vec<(&str, Option<u32>)> = engines
154            .iter()
155            .map(|e| (e.identifier.as_str(), e.order_hint))
156            .collect();
157        assert_eq!(
158            actual_order_hints, expected_order_hints,
159            "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."
160        )
161    }
162
163    #[test]
164    fn test_sort_engines_by_order_hint() {
165        let default_engine_id = None;
166        let default_private_engine_id = None;
167        let mut engines = vec![
168            create_engine("c-engine", Some(3), None),
169            create_engine("b-engine", Some(2), None),
170            create_engine("a-engine", Some(1), None),
171        ];
172        engines.sort_by(|a, b| {
173            sort(
174                default_engine_id.as_ref(),
175                default_private_engine_id.as_ref(),
176                a,
177                b,
178            )
179        });
180
181        let actual_order: Vec<&str> = engines.iter().map(|e| e.identifier.as_str()).collect();
182        let expected_order = vec!["c-engine", "b-engine", "a-engine"];
183        assert_eq!(
184            actual_order, expected_order,
185            "Should sort engines by descending order hint, with the highest order hint appearing first."
186        )
187    }
188
189    #[test]
190    fn test_sort_engines_alphabetically_without_order_hint() {
191        let default_engine_id = None;
192        let default_private_engine_id = None;
193        let mut engines = vec![
194            create_engine("c-engine", None, None),
195            create_engine("b-engine", None, None),
196            create_engine("a-engine", None, None),
197        ];
198        engines.sort_by(|a, b| {
199            sort(
200                default_engine_id.as_ref(),
201                default_private_engine_id.as_ref(),
202                a,
203                b,
204            )
205        });
206
207        let actual_order: Vec<&str> = engines.iter().map(|e| e.identifier.as_str()).collect();
208        let expected_order = vec!["a-engine", "b-engine", "c-engine"];
209        assert_eq!(
210            actual_order, expected_order,
211            "Should sort engines alphabetically when there are no order hints."
212        )
213    }
214
215    #[test]
216    fn test_sort_engines_by_order_hint_and_alphabetically() {
217        let default_engine_id = None;
218        let default_private_engine_id = None;
219        let mut engines = vec![
220            // Identifiers are the opposite order to the names, so that we
221            // can show that we are sorting alphabetically by name.
222            create_engine("d-engine", None, Some("Charlie")),
223            create_engine("e-engine", None, Some("Beta")),
224            create_engine("f-engine", None, Some("Alpha")),
225            create_engine("c-engine", Some(4), None),
226            create_engine("b-engine", Some(5), None),
227            create_engine("a-engine", Some(6), None),
228        ];
229        engines.sort_by(|a, b| {
230            sort(
231                default_engine_id.as_ref(),
232                default_private_engine_id.as_ref(),
233                a,
234                b,
235            )
236        });
237
238        let actual_order: Vec<&str> = engines.iter().map(|e| e.identifier.as_str()).collect();
239        let expected_order = vec![
240            "a-engine", "b-engine", "c-engine", "f-engine", "e-engine", "d-engine",
241        ];
242        assert_eq!(
243            actual_order, expected_order,
244            "Should sort engines by order hint before sorting alphabetically."
245        )
246    }
247
248    #[test]
249    fn test_sort_engines_with_defaults() {
250        let default_engine_id = Some("a-engine".to_string());
251        let default_private_engine_id = Some("b-engine".to_string());
252        let mut engines = vec![
253            create_engine("c-engine", Some(3), None),
254            create_engine("a-engine", Some(1), None), // Default engine should be first
255            create_engine("b-engine", Some(2), None), // Default private engine should be second
256        ];
257        engines.sort_by(|a, b| {
258            sort(
259                default_engine_id.as_ref(),
260                default_private_engine_id.as_ref(),
261                a,
262                b,
263            )
264        });
265
266        let actual_order: Vec<&str> = engines.iter().map(|e| e.identifier.as_str()).collect();
267        let expected_order = vec!["a-engine", "b-engine", "c-engine"];
268        assert_eq!(
269            actual_order, expected_order,
270            "Should have sorted the default and private default to have the highest priority."
271        )
272    }
273
274    #[test]
275    fn test_sort_engines_non_ascii_without_order_hint() {
276        // TODO: update the comparison to use ICU4X when it's available.
277        // See Bug 1945295: https://bugzilla.mozilla.org/show_bug.cgi?id=1945295
278    }
279}