1use crate::configuration_overrides_types::JSONOverridesRecord;
8use crate::environment_matching::matches_user_environment;
9use crate::{
10 error::Error, JSONDefaultEnginesRecord, JSONEngineBase, JSONEngineRecord, JSONEngineUrl,
11 JSONEngineUrls, JSONEngineVariant, JSONSearchConfigurationRecords, RefinedSearchConfig,
12 SearchEngineDefinition, SearchEngineUrl, SearchEngineUrls, SearchUserEnvironment,
13};
14use crate::{sort_helpers, JSONAvailableLocalesRecord, JSONEngineOrdersRecord};
15use remote_settings::RemoteSettingsRecord;
16use std::collections::HashSet;
17
18impl From<JSONEngineUrl> for SearchEngineUrl {
19 fn from(url: JSONEngineUrl) -> Self {
20 Self {
21 base: url.base.unwrap_or_default(),
22 method: url.method.unwrap_or_default().as_str().to_string(),
23 params: url.params.unwrap_or_default(),
24 search_term_param_name: url.search_term_param_name,
25 }
26 }
27}
28
29impl SearchEngineUrl {
30 fn merge(&mut self, preferred: &JSONEngineUrl) {
31 if let Some(base) = &preferred.base {
32 self.base = base.clone();
33 }
34 if let Some(method) = &preferred.method {
35 self.method = method.as_str().to_string();
36 }
37 if let Some(params) = &preferred.params {
38 self.params = params.clone();
39 }
40 if let Some(search_term_param_name) = &preferred.search_term_param_name {
41 self.search_term_param_name = Some(search_term_param_name.clone());
42 }
43 }
44}
45
46impl From<JSONEngineUrls> for SearchEngineUrls {
47 fn from(urls: JSONEngineUrls) -> Self {
48 Self {
49 search: urls.search.unwrap_or_default().into(),
50 suggestions: urls.suggestions.map(|suggestions| suggestions.into()),
51 trending: urls.trending.map(|trending| trending.into()),
52 search_form: urls.search_form.map(|search_form| search_form.into()),
53 }
54 }
55}
56
57impl SearchEngineUrls {
58 fn merge(&mut self, preferred: &JSONEngineUrls) {
59 if let Some(search_url) = &preferred.search {
60 self.search.merge(search_url);
61 }
62 if let Some(suggestions_url) = &preferred.suggestions {
63 match &mut self.suggestions {
64 Some(suggestion) => suggestion.merge(suggestions_url),
65 None => self.suggestions = Some(suggestions_url.clone().into()),
66 };
67 }
68 if let Some(trending_url) = &preferred.trending {
69 match &mut self.trending {
70 Some(trend) => trend.merge(trending_url),
71 None => self.trending = Some(trending_url.clone().into()),
72 };
73 }
74 if let Some(search_form_url) = &preferred.search_form {
75 match &mut self.search_form {
76 Some(search_form) => search_form.merge(search_form_url),
77 None => self.search_form = Some(search_form_url.clone().into()),
78 };
79 }
80 }
81}
82
83impl SearchEngineDefinition {
84 fn merge_variant(&mut self, variant: &JSONEngineVariant) {
85 if !self.optional {
86 self.optional = variant.optional;
87 }
88 if let Some(partner_code) = &variant.partner_code {
89 self.partner_code = partner_code.clone();
90 }
91 if let Some(telemetry_suffix) = &variant.telemetry_suffix {
92 self.telemetry_suffix = telemetry_suffix.clone();
93 }
94 if let Some(urls) = &variant.urls {
95 self.urls.merge(urls);
96 }
97 if let Some(is_new_until) = &variant.is_new_until {
98 self.is_new_until = Some(is_new_until.clone());
99 }
100 }
101
102 fn merge_override(&mut self, override_record: &JSONOverridesRecord) {
103 self.partner_code = override_record.partner_code.clone();
104 self.urls.merge(&override_record.urls);
105 self.click_url = Some(override_record.click_url.clone());
106
107 if let Some(telemetry_suffix) = &override_record.telemetry_suffix {
108 self.telemetry_suffix = telemetry_suffix.clone();
109 }
110 }
111
112 pub(crate) fn from_configuration_details(
113 identifier: &str,
114 base: JSONEngineBase,
115 variant: &JSONEngineVariant,
116 sub_variant: &Option<JSONEngineVariant>,
117 ) -> SearchEngineDefinition {
118 let mut engine_definition = SearchEngineDefinition {
119 aliases: base.aliases.unwrap_or_default(),
120 charset: base.charset.unwrap_or_else(|| "UTF-8".to_string()),
121 classification: base.classification,
122 identifier: identifier.to_string(),
123 name: base.name,
124 optional: variant.optional,
125 order_hint: None,
126 partner_code: base.partner_code.unwrap_or_default(),
127 telemetry_suffix: String::new(),
128 urls: base.urls.into(),
129 click_url: None,
130 is_new_until: None,
131 };
132
133 engine_definition.merge_variant(variant);
134 if let Some(sub_variant) = sub_variant {
135 engine_definition.merge_variant(sub_variant);
136 }
137
138 engine_definition
139 }
140}
141
142pub(crate) struct FilterRecordsResult {
143 engines: Vec<SearchEngineDefinition>,
144 default_engines_record: Option<JSONDefaultEnginesRecord>,
145 engine_orders_record: Option<JSONEngineOrdersRecord>,
146}
147
148pub(crate) trait Filter {
149 fn filter_records(
150 &self,
151 user_environment: &mut SearchUserEnvironment,
152 overrides: Option<Vec<JSONOverridesRecord>>,
153 ) -> Result<FilterRecordsResult, Error>;
154}
155
156fn apply_overrides(engines: &mut [SearchEngineDefinition], overrides: &[JSONOverridesRecord]) {
157 for override_record in overrides {
158 for engine in engines.iter_mut() {
159 if engine.identifier == override_record.identifier {
160 engine.merge_override(override_record);
161 }
162 }
163 }
164}
165
166fn negotiate_languages(user_environment: &mut SearchUserEnvironment, available_locales: &[String]) {
167 let user_locale = user_environment.locale.to_lowercase();
168
169 let available_locales_set: HashSet<String> = available_locales
170 .iter()
171 .map(|locale| locale.to_lowercase())
172 .collect();
173
174 if available_locales_set.contains(&user_locale) {
175 return;
176 }
177 if user_locale.starts_with("en-") {
178 user_environment.locale = "en-us".to_string();
179 return;
180 }
181 if let Some(index) = user_locale.find('-') {
182 let base_locale = &user_locale[..index];
183 if available_locales_set.contains(base_locale) {
184 user_environment.locale = base_locale.to_string();
185 }
186 }
187}
188
189impl Filter for Vec<RemoteSettingsRecord> {
190 fn filter_records(
191 &self,
192 user_environment: &mut SearchUserEnvironment,
193 overrides: Option<Vec<JSONOverridesRecord>>,
194 ) -> Result<FilterRecordsResult, Error> {
195 let mut available_locales = Vec::new();
196 for record in self {
197 if let Some(val) = record.fields.get("recordType") {
198 if *val == "availableLocales" {
199 let stringified = serde_json::to_string(&record.fields)?;
200 let locales_record: Option<JSONAvailableLocalesRecord> =
201 serde_json::from_str(&stringified)?;
202 available_locales = locales_record.unwrap().locales;
203 }
204 }
205 }
206 negotiate_languages(user_environment, &available_locales);
207
208 let mut engines = Vec::new();
209 let mut default_engines_record = None;
210 let mut engine_orders_record = None;
211
212 for record in self {
213 let stringified = serde_json::to_string(&record.fields)?;
216 match record.fields.get("recordType") {
217 Some(val) if *val == "engine" => {
218 let engine_config: Option<JSONEngineRecord> =
219 serde_json::from_str(&stringified)?;
220 if let Some(engine_config) = engine_config {
221 let result =
222 maybe_extract_engine_config(user_environment, Box::new(engine_config));
223 engines.extend(result);
224 }
225 }
226 Some(val) if *val == "defaultEngines" => {
227 default_engines_record = serde_json::from_str(&stringified)?;
228 }
229 Some(val) if *val == "engineOrders" => {
230 engine_orders_record = serde_json::from_str(&stringified)?;
231 }
232 Some(val) if *val == "availableLocales" => {
233 }
235 Some(_val) => {}
238 None => {}
239 }
240 }
241
242 if let Some(overrides_data) = &overrides {
243 apply_overrides(&mut engines, overrides_data);
244 }
245
246 Ok(FilterRecordsResult {
247 engines,
248 default_engines_record,
249 engine_orders_record,
250 })
251 }
252}
253
254impl Filter for Vec<JSONSearchConfigurationRecords> {
255 fn filter_records(
256 &self,
257 user_environment: &mut SearchUserEnvironment,
258 overrides: Option<Vec<JSONOverridesRecord>>,
259 ) -> Result<FilterRecordsResult, Error> {
260 let mut available_locales = Vec::new();
261 for record in self {
262 if let JSONSearchConfigurationRecords::AvailableLocales(locales_record) = record {
263 available_locales = locales_record.locales.clone();
264 }
265 }
266 negotiate_languages(user_environment, &available_locales);
267
268 let mut engines = Vec::new();
269 let mut default_engines_record = None;
270 let mut engine_orders_record = None;
271
272 for record in self {
273 match record {
274 JSONSearchConfigurationRecords::Engine(engine) => {
275 let result = maybe_extract_engine_config(user_environment, engine.clone());
276 engines.extend(result);
277 }
278 JSONSearchConfigurationRecords::DefaultEngines(default_engines) => {
279 default_engines_record = Some(default_engines);
280 }
281 JSONSearchConfigurationRecords::EngineOrders(engine_orders) => {
282 engine_orders_record = Some(engine_orders)
283 }
284 JSONSearchConfigurationRecords::AvailableLocales(_) => {
285 }
287 JSONSearchConfigurationRecords::Unknown => {
288 }
290 }
291 }
292
293 if let Some(overrides_data) = &overrides {
294 apply_overrides(&mut engines, overrides_data);
295 }
296
297 Ok(FilterRecordsResult {
298 engines,
299 default_engines_record: default_engines_record.cloned(),
300 engine_orders_record: engine_orders_record.cloned(),
301 })
302 }
303}
304
305pub(crate) fn filter_engine_configuration_impl(
306 user_environment: SearchUserEnvironment,
307 configuration: &impl Filter,
308 overrides: Option<Vec<JSONOverridesRecord>>,
309) -> Result<RefinedSearchConfig, Error> {
310 let mut user_environment = user_environment.clone();
311 user_environment.locale = user_environment.locale.to_lowercase();
312 user_environment.region = user_environment.region.to_lowercase();
313 user_environment.version = user_environment.version.to_lowercase();
314
315 let filtered_result = configuration.filter_records(&mut user_environment, overrides);
316
317 filtered_result.map(|result| {
318 let (default_engine_id, default_private_engine_id) = determine_default_engines(
319 &result.engines,
320 result.default_engines_record,
321 &user_environment,
322 );
323
324 let mut engines = result.engines.clone();
325
326 if let Some(orders_record) = result.engine_orders_record {
327 for order_data in &orders_record.orders {
328 if matches_user_environment(&order_data.environment, &user_environment) {
329 sort_helpers::set_engine_order(&mut engines, &order_data.order);
330 }
331 }
332 }
333
334 engines.sort_by(|a, b| {
335 sort_helpers::sort(
336 default_engine_id.as_ref(),
337 default_private_engine_id.as_ref(),
338 a,
339 b,
340 )
341 });
342
343 RefinedSearchConfig {
344 engines,
345 app_default_engine_id: default_engine_id,
346 app_private_default_engine_id: default_private_engine_id,
347 }
348 })
349}
350
351fn maybe_extract_engine_config(
352 user_environment: &SearchUserEnvironment,
353 record: Box<JSONEngineRecord>,
354) -> Option<SearchEngineDefinition> {
355 let JSONEngineRecord {
356 identifier,
357 variants,
358 base,
359 } = *record;
360 let matching_variant = variants
361 .into_iter()
362 .rev()
363 .find(|r| matches_user_environment(&r.environment, user_environment));
364
365 let mut matching_sub_variant = None;
366 if let Some(variant) = &matching_variant {
367 matching_sub_variant = variant
368 .sub_variants
369 .iter()
370 .rev()
371 .find(|r| matches_user_environment(&r.environment, user_environment))
372 .cloned();
373 }
374
375 matching_variant.map(|variant| {
376 SearchEngineDefinition::from_configuration_details(
377 &identifier,
378 base,
379 &variant,
380 &matching_sub_variant,
381 )
382 })
383}
384
385fn determine_default_engines(
386 engines: &[SearchEngineDefinition],
387 default_engines_record: Option<JSONDefaultEnginesRecord>,
388 user_environment: &SearchUserEnvironment,
389) -> (Option<String>, Option<String>) {
390 match default_engines_record {
391 None => (None, None),
392 Some(record) => {
393 let mut default_engine_id = None;
394 let mut default_engine_private_id = None;
395
396 let specific_default = record
397 .specific_defaults
398 .into_iter()
399 .rev()
400 .find(|r| matches_user_environment(&r.environment, user_environment));
401
402 if let Some(specific_default) = specific_default {
403 if let Some(engine_id) =
406 find_engine_id_with_match(engines, specific_default.default)
407 {
408 default_engine_id.replace(engine_id);
409 }
410 if let Some(private_engine_id) =
411 find_engine_id_with_match(engines, specific_default.default_private)
412 {
413 default_engine_private_id.replace(private_engine_id);
414 }
415 }
416
417 (
418 default_engine_id.or_else(|| find_engine_id(engines, record.global_default)),
427 default_engine_private_id
428 .or_else(|| find_engine_id(engines, record.global_default_private)),
429 )
430 }
431 }
432}
433
434fn find_engine_id(engines: &[SearchEngineDefinition], engine_id: String) -> Option<String> {
435 if engine_id.is_empty() {
436 return None;
437 }
438 match engines.iter().any(|e| e.identifier == engine_id) {
439 true => Some(engine_id.clone()),
440 false => None,
441 }
442}
443
444fn find_engine_id_with_match(
445 engines: &[SearchEngineDefinition],
446 engine_id_match: String,
447) -> Option<String> {
448 if engine_id_match.is_empty() {
449 return None;
450 }
451 if let Some(match_no_star) = engine_id_match.strip_suffix('*') {
452 return engines
453 .iter()
454 .find(|e| e.identifier.starts_with(match_no_star))
455 .map(|e| e.identifier.clone());
456 }
457
458 engines
459 .iter()
460 .find(|e| e.identifier == engine_id_match)
461 .map(|e| e.identifier.clone())
462}
463
464#[cfg(test)]
465mod tests {
466 use std::vec;
467
468 use super::*;
469 use crate::*;
470 use once_cell::sync::Lazy;
471 use pretty_assertions::assert_eq;
472
473 #[test]
474 fn test_merge_override() {
475 let mut test_engine = SearchEngineDefinition {
476 identifier: "test".to_string(),
477 partner_code: "partner-code".to_string(),
478 telemetry_suffix: "original-telemetry-suffix".to_string(),
479 ..Default::default()
480 };
481
482 let override_record = JSONOverridesRecord {
483 identifier: "test".to_string(),
484 partner_code: "override-partner-code".to_string(),
485 click_url: "https://example.com/click-url".to_string(),
486 telemetry_suffix: None,
487 urls: JSONEngineUrls {
488 search: Some(JSONEngineUrl {
489 base: Some("https://example.com/override-search".to_string()),
490 method: None,
491 params: None,
492 search_term_param_name: None,
493 }),
494 ..Default::default()
495 },
496 };
497
498 test_engine.merge_override(&override_record);
499
500 assert_eq!(
501 test_engine.partner_code, "override-partner-code",
502 "Should override the partner code"
503 );
504 assert_eq!(
505 test_engine.click_url,
506 Some("https://example.com/click-url".to_string()),
507 "Should override the click url"
508 );
509 assert_eq!(
510 test_engine.urls.search.base, "https://example.com/override-search",
511 "Should override search url"
512 );
513 assert_eq!(
514 test_engine.telemetry_suffix, "original-telemetry-suffix",
515 "Should not override telemetry suffix when telemetry suffix is supplied as None"
516 );
517 }
518
519 #[test]
520 fn test_from_configuration_details_fallsback_to_defaults() {
521 let result = SearchEngineDefinition::from_configuration_details(
525 "test",
526 JSONEngineBase {
527 aliases: None,
528 charset: None,
529 classification: SearchEngineClassification::General,
530 name: "Test".to_string(),
531 partner_code: None,
532 urls: JSONEngineUrls {
533 search: Some(JSONEngineUrl {
534 base: Some("https://example.com".to_string()),
535 method: None,
536 params: None,
537 search_term_param_name: None,
538 }),
539 suggestions: None,
540 trending: None,
541 search_form: None,
542 },
543 },
544 &JSONEngineVariant {
545 environment: JSONVariantEnvironment {
546 all_regions_and_locales: true,
547 ..Default::default()
548 },
549 is_new_until: None,
550 optional: false,
551 partner_code: None,
552 telemetry_suffix: None,
553 urls: None,
554 sub_variants: vec![],
555 },
556 &None,
557 );
558
559 assert_eq!(
560 result,
561 SearchEngineDefinition {
562 aliases: Vec::new(),
563 charset: "UTF-8".to_string(),
564 classification: SearchEngineClassification::General,
565 identifier: "test".to_string(),
566 is_new_until: None,
567 partner_code: String::new(),
568 name: "Test".to_string(),
569 optional: false,
570 order_hint: None,
571 telemetry_suffix: String::new(),
572 urls: SearchEngineUrls {
573 search: SearchEngineUrl {
574 base: "https://example.com".to_string(),
575 method: "GET".to_string(),
576 params: Vec::new(),
577 search_term_param_name: None,
578 },
579 suggestions: None,
580 trending: None,
581 search_form: None
582 },
583 click_url: None
584 }
585 )
586 }
587
588 static ENGINE_BASE: Lazy<JSONEngineBase> = Lazy::new(|| JSONEngineBase {
589 aliases: Some(vec!["foo".to_string(), "bar".to_string()]),
590 charset: Some("ISO-8859-15".to_string()),
591 classification: SearchEngineClassification::Unknown,
592 name: "Test".to_string(),
593 partner_code: Some("firefox".to_string()),
594 urls: JSONEngineUrls {
595 search: Some(JSONEngineUrl {
596 base: Some("https://example.com".to_string()),
597 method: Some(crate::JSONEngineMethod::Post),
598 params: Some(vec![
599 SearchUrlParam {
600 name: "param".to_string(),
601 value: Some("test param".to_string()),
602 enterprise_value: None,
603 experiment_config: None,
604 },
605 SearchUrlParam {
606 name: "enterprise-name".to_string(),
607 value: None,
608 enterprise_value: Some("enterprise-value".to_string()),
609 experiment_config: None,
610 },
611 ]),
612 search_term_param_name: Some("baz".to_string()),
613 }),
614 suggestions: Some(JSONEngineUrl {
615 base: Some("https://example.com/suggestions".to_string()),
616 method: Some(crate::JSONEngineMethod::Get),
617 params: Some(vec![SearchUrlParam {
618 name: "suggest-name".to_string(),
619 value: None,
620 enterprise_value: None,
621 experiment_config: Some("suggest-experiment-value".to_string()),
622 }]),
623 search_term_param_name: Some("suggest".to_string()),
624 }),
625 trending: Some(JSONEngineUrl {
626 base: Some("https://example.com/trending".to_string()),
627 method: Some(crate::JSONEngineMethod::Get),
628 params: Some(vec![SearchUrlParam {
629 name: "trend-name".to_string(),
630 value: Some("trend-value".to_string()),
631 enterprise_value: None,
632 experiment_config: None,
633 }]),
634 search_term_param_name: None,
635 }),
636 search_form: Some(JSONEngineUrl {
637 base: Some("https://example.com/search_form".to_string()),
638 method: Some(crate::JSONEngineMethod::Get),
639 params: Some(vec![SearchUrlParam {
640 name: "search-form-name".to_string(),
641 value: Some("search-form-value".to_string()),
642 enterprise_value: None,
643 experiment_config: None,
644 }]),
645 search_term_param_name: None,
646 }),
647 },
648 });
649
650 #[test]
651 fn test_from_configuration_details_uses_values() {
652 let result = SearchEngineDefinition::from_configuration_details(
653 "test",
654 Lazy::force(&ENGINE_BASE).clone(),
655 &JSONEngineVariant {
656 environment: JSONVariantEnvironment {
657 all_regions_and_locales: true,
658 ..Default::default()
659 },
660 is_new_until: None,
661 optional: false,
662 partner_code: None,
663 telemetry_suffix: None,
664 urls: None,
665 sub_variants: vec![],
666 },
667 &None,
668 );
669
670 assert_eq!(
671 result,
672 SearchEngineDefinition {
673 aliases: vec!["foo".to_string(), "bar".to_string()],
674 charset: "ISO-8859-15".to_string(),
675 classification: SearchEngineClassification::Unknown,
676 identifier: "test".to_string(),
677 is_new_until: None,
678 partner_code: "firefox".to_string(),
679 name: "Test".to_string(),
680 optional: false,
681 order_hint: None,
682 telemetry_suffix: String::new(),
683 urls: SearchEngineUrls {
684 search: SearchEngineUrl {
685 base: "https://example.com".to_string(),
686 method: "POST".to_string(),
687 params: vec![
688 SearchUrlParam {
689 name: "param".to_string(),
690 value: Some("test param".to_string()),
691 enterprise_value: None,
692 experiment_config: None,
693 },
694 SearchUrlParam {
695 name: "enterprise-name".to_string(),
696 value: None,
697 enterprise_value: Some("enterprise-value".to_string()),
698 experiment_config: None,
699 },
700 ],
701 search_term_param_name: Some("baz".to_string()),
702 },
703 suggestions: Some(SearchEngineUrl {
704 base: "https://example.com/suggestions".to_string(),
705 method: "GET".to_string(),
706 params: vec![SearchUrlParam {
707 name: "suggest-name".to_string(),
708 value: None,
709 enterprise_value: None,
710 experiment_config: Some("suggest-experiment-value".to_string()),
711 }],
712 search_term_param_name: Some("suggest".to_string()),
713 }),
714 trending: Some(SearchEngineUrl {
715 base: "https://example.com/trending".to_string(),
716 method: "GET".to_string(),
717 params: vec![SearchUrlParam {
718 name: "trend-name".to_string(),
719 value: Some("trend-value".to_string()),
720 enterprise_value: None,
721 experiment_config: None,
722 }],
723 search_term_param_name: None,
724 }),
725 search_form: Some(SearchEngineUrl {
726 base: "https://example.com/search_form".to_string(),
727 method: "GET".to_string(),
728 params: vec![SearchUrlParam {
729 name: "search-form-name".to_string(),
730 value: Some("search-form-value".to_string()),
731 enterprise_value: None,
732 experiment_config: None,
733 }],
734 search_term_param_name: None,
735 }),
736 },
737 click_url: None
738 }
739 )
740 }
741
742 #[test]
743 fn test_from_configuration_details_merges_variants() {
744 let result = SearchEngineDefinition::from_configuration_details(
745 "test",
746 Lazy::force(&ENGINE_BASE).clone(),
747 &JSONEngineVariant {
748 environment: JSONVariantEnvironment {
749 all_regions_and_locales: true,
750 ..Default::default()
751 },
752 is_new_until: Some("2063-04-05".to_string()),
753 optional: true,
754 partner_code: Some("trek".to_string()),
755 telemetry_suffix: Some("star".to_string()),
756 urls: Some(JSONEngineUrls {
757 search: Some(JSONEngineUrl {
758 base: Some("https://example.com/variant".to_string()),
759 method: Some(JSONEngineMethod::Get),
760 params: Some(vec![SearchUrlParam {
761 name: "variant".to_string(),
762 value: Some("test variant".to_string()),
763 enterprise_value: None,
764 experiment_config: None,
765 }]),
766 search_term_param_name: Some("ship".to_string()),
767 }),
768 suggestions: Some(JSONEngineUrl {
769 base: Some("https://example.com/suggestions-variant".to_string()),
770 method: Some(JSONEngineMethod::Get),
771 params: Some(vec![SearchUrlParam {
772 name: "suggest-variant".to_string(),
773 value: Some("sugg test variant".to_string()),
774 enterprise_value: None,
775 experiment_config: None,
776 }]),
777 search_term_param_name: Some("variant".to_string()),
778 }),
779 trending: Some(JSONEngineUrl {
780 base: Some("https://example.com/trending-variant".to_string()),
781 method: Some(JSONEngineMethod::Get),
782 params: Some(vec![SearchUrlParam {
783 name: "trend-variant".to_string(),
784 value: Some("trend test variant".to_string()),
785 enterprise_value: None,
786 experiment_config: None,
787 }]),
788 search_term_param_name: Some("trend".to_string()),
789 }),
790 search_form: Some(JSONEngineUrl {
791 base: Some("https://example.com/search_form".to_string()),
792 method: Some(crate::JSONEngineMethod::Get),
793 params: Some(vec![SearchUrlParam {
794 name: "search-form-name".to_string(),
795 value: Some("search-form-value".to_string()),
796 enterprise_value: None,
797 experiment_config: None,
798 }]),
799 search_term_param_name: None,
800 }),
801 }),
802 sub_variants: vec![],
803 },
804 &None,
805 );
806
807 assert_eq!(
808 result,
809 SearchEngineDefinition {
810 aliases: vec!["foo".to_string(), "bar".to_string()],
811 charset: "ISO-8859-15".to_string(),
812 classification: SearchEngineClassification::Unknown,
813 identifier: "test".to_string(),
814 is_new_until: Some("2063-04-05".to_string()),
815 partner_code: "trek".to_string(),
816 name: "Test".to_string(),
817 optional: true,
818 order_hint: None,
819 telemetry_suffix: "star".to_string(),
820 urls: SearchEngineUrls {
821 search: SearchEngineUrl {
822 base: "https://example.com/variant".to_string(),
823 method: "GET".to_string(),
824 params: vec![SearchUrlParam {
825 name: "variant".to_string(),
826 value: Some("test variant".to_string()),
827 enterprise_value: None,
828 experiment_config: None,
829 }],
830 search_term_param_name: Some("ship".to_string()),
831 },
832 suggestions: Some(SearchEngineUrl {
833 base: "https://example.com/suggestions-variant".to_string(),
834 method: "GET".to_string(),
835 params: vec![SearchUrlParam {
836 name: "suggest-variant".to_string(),
837 value: Some("sugg test variant".to_string()),
838 enterprise_value: None,
839 experiment_config: None,
840 }],
841 search_term_param_name: Some("variant".to_string()),
842 }),
843 trending: Some(SearchEngineUrl {
844 base: "https://example.com/trending-variant".to_string(),
845 method: "GET".to_string(),
846 params: vec![SearchUrlParam {
847 name: "trend-variant".to_string(),
848 value: Some("trend test variant".to_string()),
849 enterprise_value: None,
850 experiment_config: None,
851 }],
852 search_term_param_name: Some("trend".to_string()),
853 }),
854 search_form: Some(SearchEngineUrl {
855 base: "https://example.com/search_form".to_string(),
856 method: "GET".to_string(),
857 params: vec![SearchUrlParam {
858 name: "search-form-name".to_string(),
859 value: Some("search-form-value".to_string()),
860 enterprise_value: None,
861 experiment_config: None,
862 }],
863 search_term_param_name: None,
864 }),
865 },
866 click_url: None
867 }
868 )
869 }
870
871 #[test]
872 fn test_from_configuration_details_merges_sub_variants() {
873 let result = SearchEngineDefinition::from_configuration_details(
874 "test",
875 Lazy::force(&ENGINE_BASE).clone(),
876 &JSONEngineVariant {
877 environment: JSONVariantEnvironment {
878 all_regions_and_locales: true,
879 ..Default::default()
880 },
881 is_new_until: None,
882 optional: true,
883 partner_code: Some("trek".to_string()),
884 telemetry_suffix: Some("star".to_string()),
885 urls: Some(JSONEngineUrls {
886 search: Some(JSONEngineUrl {
887 base: Some("https://example.com/variant".to_string()),
888 method: Some(JSONEngineMethod::Get),
889 params: Some(vec![SearchUrlParam {
890 name: "variant".to_string(),
891 value: Some("test variant".to_string()),
892 enterprise_value: None,
893 experiment_config: None,
894 }]),
895 search_term_param_name: Some("ship".to_string()),
896 }),
897 suggestions: Some(JSONEngineUrl {
898 base: Some("https://example.com/suggestions-variant".to_string()),
899 method: Some(JSONEngineMethod::Get),
900 params: Some(vec![SearchUrlParam {
901 name: "suggest-variant".to_string(),
902 value: Some("sugg test variant".to_string()),
903 enterprise_value: None,
904 experiment_config: None,
905 }]),
906 search_term_param_name: Some("variant".to_string()),
907 }),
908 trending: Some(JSONEngineUrl {
909 base: Some("https://example.com/trending-variant".to_string()),
910 method: Some(JSONEngineMethod::Get),
911 params: Some(vec![SearchUrlParam {
912 name: "trend-variant".to_string(),
913 value: Some("trend test variant".to_string()),
914 enterprise_value: None,
915 experiment_config: None,
916 }]),
917 search_term_param_name: Some("trend".to_string()),
918 }),
919 search_form: Some(JSONEngineUrl {
920 base: Some("https://example.com/search-form-variant".to_string()),
921 method: Some(crate::JSONEngineMethod::Get),
922 params: Some(vec![SearchUrlParam {
923 name: "search-form-variant".to_string(),
924 value: Some("search form variant".to_string()),
925 enterprise_value: None,
926 experiment_config: None,
927 }]),
928 search_term_param_name: None,
929 }),
930 }),
931 sub_variants: vec![],
936 },
937 &Some(JSONEngineVariant {
938 environment: JSONVariantEnvironment {
939 all_regions_and_locales: true,
940 ..Default::default()
941 },
942 is_new_until: Some("2063-04-05".to_string()),
943 optional: true,
944 partner_code: Some("trek2".to_string()),
945 telemetry_suffix: Some("star2".to_string()),
946 urls: Some(JSONEngineUrls {
947 search: Some(JSONEngineUrl {
948 base: Some("https://example.com/subvariant".to_string()),
949 method: Some(JSONEngineMethod::Get),
950 params: Some(vec![SearchUrlParam {
951 name: "subvariant".to_string(),
952 value: Some("test subvariant".to_string()),
953 enterprise_value: None,
954 experiment_config: None,
955 }]),
956 search_term_param_name: Some("shuttle".to_string()),
957 }),
958 suggestions: Some(JSONEngineUrl {
959 base: Some("https://example.com/suggestions-subvariant".to_string()),
960 method: Some(JSONEngineMethod::Get),
961 params: Some(vec![SearchUrlParam {
962 name: "suggest-subvariant".to_string(),
963 value: Some("sugg test subvariant".to_string()),
964 enterprise_value: None,
965 experiment_config: None,
966 }]),
967 search_term_param_name: Some("subvariant".to_string()),
968 }),
969 trending: Some(JSONEngineUrl {
970 base: Some("https://example.com/trending-subvariant".to_string()),
971 method: Some(JSONEngineMethod::Get),
972 params: Some(vec![SearchUrlParam {
973 name: "trend-subvariant".to_string(),
974 value: Some("trend test subvariant".to_string()),
975 enterprise_value: None,
976 experiment_config: None,
977 }]),
978 search_term_param_name: Some("subtrend".to_string()),
979 }),
980 search_form: Some(JSONEngineUrl {
981 base: Some("https://example.com/search-form-subvariant".to_string()),
982 method: Some(crate::JSONEngineMethod::Get),
983 params: Some(vec![SearchUrlParam {
984 name: "search-form-subvariant".to_string(),
985 value: Some("search form subvariant".to_string()),
986 enterprise_value: None,
987 experiment_config: None,
988 }]),
989 search_term_param_name: None,
990 }),
991 }),
992 sub_variants: vec![],
993 }),
994 );
995
996 assert_eq!(
997 result,
998 SearchEngineDefinition {
999 aliases: vec!["foo".to_string(), "bar".to_string()],
1000 charset: "ISO-8859-15".to_string(),
1001 classification: SearchEngineClassification::Unknown,
1002 identifier: "test".to_string(),
1003 is_new_until: Some("2063-04-05".to_string()),
1004 partner_code: "trek2".to_string(),
1005 name: "Test".to_string(),
1006 optional: true,
1007 order_hint: None,
1008 telemetry_suffix: "star2".to_string(),
1009 urls: SearchEngineUrls {
1010 search: SearchEngineUrl {
1011 base: "https://example.com/subvariant".to_string(),
1012 method: "GET".to_string(),
1013 params: vec![SearchUrlParam {
1014 name: "subvariant".to_string(),
1015 value: Some("test subvariant".to_string()),
1016 enterprise_value: None,
1017 experiment_config: None,
1018 }],
1019 search_term_param_name: Some("shuttle".to_string()),
1020 },
1021 suggestions: Some(SearchEngineUrl {
1022 base: "https://example.com/suggestions-subvariant".to_string(),
1023 method: "GET".to_string(),
1024 params: vec![SearchUrlParam {
1025 name: "suggest-subvariant".to_string(),
1026 value: Some("sugg test subvariant".to_string()),
1027 enterprise_value: None,
1028 experiment_config: None,
1029 }],
1030 search_term_param_name: Some("subvariant".to_string()),
1031 }),
1032 trending: Some(SearchEngineUrl {
1033 base: "https://example.com/trending-subvariant".to_string(),
1034 method: "GET".to_string(),
1035 params: vec![SearchUrlParam {
1036 name: "trend-subvariant".to_string(),
1037 value: Some("trend test subvariant".to_string()),
1038 enterprise_value: None,
1039 experiment_config: None,
1040 }],
1041 search_term_param_name: Some("subtrend".to_string()),
1042 }),
1043 search_form: Some(SearchEngineUrl {
1044 base: "https://example.com/search-form-subvariant".to_string(),
1045 method: "GET".to_string(),
1046 params: vec![SearchUrlParam {
1047 name: "search-form-subvariant".to_string(),
1048 value: Some("search form subvariant".to_string()),
1049 enterprise_value: None,
1050 experiment_config: None,
1051 }],
1052 search_term_param_name: None,
1053 }),
1054 },
1055 click_url: None
1056 }
1057 )
1058 }
1059
1060 static ENGINES_LIST: Lazy<Vec<SearchEngineDefinition>> = Lazy::new(|| {
1061 vec![
1062 SearchEngineDefinition {
1063 identifier: "engine1".to_string(),
1064 name: "Test".to_string(),
1065 urls: SearchEngineUrls {
1066 search: SearchEngineUrl {
1067 base: "https://example.com".to_string(),
1068 method: "GET".to_string(),
1069 params: Vec::new(),
1070 search_term_param_name: None,
1071 },
1072 ..Default::default()
1073 },
1074 ..Default::default()
1075 },
1076 SearchEngineDefinition {
1077 identifier: "engine2".to_string(),
1078 name: "Test 2".to_string(),
1079 urls: SearchEngineUrls {
1080 search: SearchEngineUrl {
1081 base: "https://example.com/2".to_string(),
1082 method: "GET".to_string(),
1083 params: Vec::new(),
1084 search_term_param_name: None,
1085 },
1086 ..Default::default()
1087 },
1088 ..Default::default()
1089 },
1090 SearchEngineDefinition {
1091 identifier: "engine3".to_string(),
1092 name: "Test 3".to_string(),
1093 urls: SearchEngineUrls {
1094 search: SearchEngineUrl {
1095 base: "https://example.com/3".to_string(),
1096 method: "GET".to_string(),
1097 params: Vec::new(),
1098 search_term_param_name: None,
1099 },
1100 ..Default::default()
1101 },
1102 ..Default::default()
1103 },
1104 SearchEngineDefinition {
1105 identifier: "engine4wildcardmatch".to_string(),
1106 name: "Test 4".to_string(),
1107 urls: SearchEngineUrls {
1108 search: SearchEngineUrl {
1109 base: "https://example.com/4".to_string(),
1110 method: "GET".to_string(),
1111 params: Vec::new(),
1112 search_term_param_name: None,
1113 },
1114 ..Default::default()
1115 },
1116 ..Default::default()
1117 },
1118 ]
1119 });
1120
1121 #[test]
1122 fn test_determine_default_engines_returns_global_default() {
1123 let (default_engine_id, default_engine_private_id) = determine_default_engines(
1124 &ENGINES_LIST,
1125 Some(JSONDefaultEnginesRecord {
1126 global_default: "engine2".to_string(),
1127 global_default_private: String::new(),
1128 specific_defaults: Vec::new(),
1129 }),
1130 &SearchUserEnvironment {
1131 locale: "fi".into(),
1132 ..Default::default()
1133 },
1134 );
1135
1136 assert_eq!(
1137 default_engine_id.unwrap(),
1138 "engine2",
1139 "Should have returned the global default engine"
1140 );
1141 assert!(
1142 default_engine_private_id.is_none(),
1143 "Should not have returned an id for the private engine"
1144 );
1145
1146 let (default_engine_id, default_engine_private_id) = determine_default_engines(
1147 &ENGINES_LIST,
1148 Some(JSONDefaultEnginesRecord {
1149 global_default: "engine2".to_string(),
1150 global_default_private: String::new(),
1151 specific_defaults: vec![JSONSpecificDefaultRecord {
1152 default: "engine1".to_string(),
1153 default_private: String::new(),
1154 environment: JSONVariantEnvironment {
1155 locales: vec!["en-GB".to_string()],
1156 ..Default::default()
1157 },
1158 }],
1159 }),
1160 &SearchUserEnvironment {
1161 locale: "fi".into(),
1162 ..Default::default()
1163 },
1164 );
1165
1166 assert_eq!(
1167 default_engine_id.unwrap(),
1168 "engine2",
1169 "Should have returned the global default engine when no specific defaults environments match"
1170 );
1171 assert!(
1172 default_engine_private_id.is_none(),
1173 "Should not have returned an id for the private engine"
1174 );
1175
1176 let (default_engine_id, default_engine_private_id) = determine_default_engines(
1177 &ENGINES_LIST,
1178 Some(JSONDefaultEnginesRecord {
1179 global_default: "engine2".to_string(),
1180 global_default_private: String::new(),
1181 specific_defaults: vec![JSONSpecificDefaultRecord {
1182 default: "engine1".to_string(),
1183 default_private: String::new(),
1184 environment: JSONVariantEnvironment {
1185 locales: vec!["fi".to_string()],
1186 ..Default::default()
1187 },
1188 }],
1189 }),
1190 &SearchUserEnvironment {
1191 locale: "fi".into(),
1192 ..Default::default()
1193 },
1194 );
1195
1196 assert_eq!(
1197 default_engine_id.unwrap(),
1198 "engine1",
1199 "Should have returned the specific default when environments match"
1200 );
1201 assert!(
1202 default_engine_private_id.is_none(),
1203 "Should not have returned an id for the private engine"
1204 );
1205
1206 let (default_engine_id, default_engine_private_id) = determine_default_engines(
1207 &ENGINES_LIST,
1208 Some(JSONDefaultEnginesRecord {
1209 global_default: "engine2".to_string(),
1210 global_default_private: String::new(),
1211 specific_defaults: vec![JSONSpecificDefaultRecord {
1212 default: "engine4*".to_string(),
1213 default_private: String::new(),
1214 environment: JSONVariantEnvironment {
1215 locales: vec!["fi".to_string()],
1216 ..Default::default()
1217 },
1218 }],
1219 }),
1220 &SearchUserEnvironment {
1221 locale: "fi".into(),
1222 ..Default::default()
1223 },
1224 );
1225
1226 assert_eq!(
1227 default_engine_id.unwrap(),
1228 "engine4wildcardmatch",
1229 "Should have returned the specific default when using a wildcard match"
1230 );
1231 assert!(
1232 default_engine_private_id.is_none(),
1233 "Should not have returned an id for the private engine"
1234 );
1235
1236 let (default_engine_id, default_engine_private_id) = determine_default_engines(
1237 &ENGINES_LIST,
1238 Some(JSONDefaultEnginesRecord {
1239 global_default: "engine2".to_string(),
1240 global_default_private: String::new(),
1241 specific_defaults: vec![
1242 JSONSpecificDefaultRecord {
1243 default: "engine4*".to_string(),
1244 default_private: String::new(),
1245 environment: JSONVariantEnvironment {
1246 locales: vec!["fi".to_string()],
1247 ..Default::default()
1248 },
1249 },
1250 JSONSpecificDefaultRecord {
1251 default: "engine3".to_string(),
1252 default_private: String::new(),
1253 environment: JSONVariantEnvironment {
1254 locales: vec!["fi".to_string()],
1255 ..Default::default()
1256 },
1257 },
1258 ],
1259 }),
1260 &SearchUserEnvironment {
1261 locale: "fi".into(),
1262 ..Default::default()
1263 },
1264 );
1265
1266 assert_eq!(
1267 default_engine_id.unwrap(),
1268 "engine3",
1269 "Should have returned the last specific default when multiple environments match"
1270 );
1271 assert!(
1272 default_engine_private_id.is_none(),
1273 "Should not have returned an id for the private engine"
1274 );
1275 }
1276
1277 #[test]
1278 fn test_determine_default_engines_returns_global_default_private() {
1279 let (default_engine_id, default_engine_private_id) = determine_default_engines(
1280 &ENGINES_LIST,
1281 Some(JSONDefaultEnginesRecord {
1282 global_default: "engine2".to_string(),
1283 global_default_private: "engine3".to_string(),
1284 specific_defaults: Vec::new(),
1285 }),
1286 &SearchUserEnvironment {
1287 ..Default::default()
1288 },
1289 );
1290
1291 assert_eq!(
1292 default_engine_id.unwrap(),
1293 "engine2",
1294 "Should have returned the global default engine"
1295 );
1296 assert_eq!(
1297 default_engine_private_id.unwrap(),
1298 "engine3",
1299 "Should have returned the global default engine for private mode"
1300 );
1301
1302 let (default_engine_id, default_engine_private_id) = determine_default_engines(
1303 &ENGINES_LIST,
1304 Some(JSONDefaultEnginesRecord {
1305 global_default: "engine2".to_string(),
1306 global_default_private: "engine3".to_string(),
1307 specific_defaults: vec![JSONSpecificDefaultRecord {
1308 default: String::new(),
1309 default_private: "engine1".to_string(),
1310 environment: JSONVariantEnvironment {
1311 locales: vec!["en-GB".to_string()],
1312 ..Default::default()
1313 },
1314 }],
1315 }),
1316 &SearchUserEnvironment {
1317 locale: "fi".into(),
1318 ..Default::default()
1319 },
1320 );
1321
1322 assert_eq!(
1323 default_engine_id.unwrap(),
1324 "engine2",
1325 "Should have returned the global default engine when no specific defaults environments match"
1326 );
1327 assert_eq!(
1328 default_engine_private_id.unwrap(),
1329 "engine3",
1330 "Should have returned the global default engine for private mode when no specific defaults environments match"
1331 );
1332
1333 let (default_engine_id, default_engine_private_id) = determine_default_engines(
1334 &ENGINES_LIST,
1335 Some(JSONDefaultEnginesRecord {
1336 global_default: "engine2".to_string(),
1337 global_default_private: "engine3".to_string(),
1338 specific_defaults: vec![JSONSpecificDefaultRecord {
1339 default: String::new(),
1340 default_private: "engine1".to_string(),
1341 environment: JSONVariantEnvironment {
1342 locales: vec!["fi".to_string()],
1343 ..Default::default()
1344 },
1345 }],
1346 }),
1347 &SearchUserEnvironment {
1348 locale: "fi".into(),
1349 ..Default::default()
1350 },
1351 );
1352
1353 assert_eq!(
1354 default_engine_id.unwrap(),
1355 "engine2",
1356 "Should have returned the global default engine when specific environments match which override the private global default (and not the global default)."
1357 );
1358 assert_eq!(
1359 default_engine_private_id.unwrap(),
1360 "engine1",
1361 "Should have returned the specific default engine for private mode when environments match"
1362 );
1363
1364 let (default_engine_id, default_engine_private_id) = determine_default_engines(
1365 &ENGINES_LIST,
1366 Some(JSONDefaultEnginesRecord {
1367 global_default: "engine2".to_string(),
1368 global_default_private: String::new(),
1369 specific_defaults: vec![JSONSpecificDefaultRecord {
1370 default: String::new(),
1371 default_private: "engine4*".to_string(),
1372 environment: JSONVariantEnvironment {
1373 locales: vec!["fi".to_string()],
1374 ..Default::default()
1375 },
1376 }],
1377 }),
1378 &SearchUserEnvironment {
1379 locale: "fi".into(),
1380 ..Default::default()
1381 },
1382 );
1383
1384 assert_eq!(
1385 default_engine_id.unwrap(),
1386 "engine2",
1387 "Should have returned the global default engine when specific environments match which override the private global default (and not the global default)"
1388 );
1389 assert_eq!(
1390 default_engine_private_id.unwrap(),
1391 "engine4wildcardmatch",
1392 "Should have returned the specific default for private mode when using a wildcard match"
1393 );
1394 }
1395
1396 #[test]
1397 fn test_locale_matched_exactly() {
1398 let mut user_env = SearchUserEnvironment {
1399 locale: "en-CA".into(),
1400 ..Default::default()
1401 };
1402 negotiate_languages(&mut user_env, &["en-CA".to_string(), "fr".to_string()]);
1403 assert_eq!(
1404 user_env.locale, "en-CA",
1405 "Should return user locale unchanged if in available locales"
1406 );
1407 }
1408
1409 #[test]
1410 fn test_locale_fallback_to_base_locale() {
1411 let mut user_env = SearchUserEnvironment {
1412 locale: "de-AT".into(),
1413 ..Default::default()
1414 };
1415 negotiate_languages(&mut user_env, &["de".to_string()]);
1416 assert_eq!(
1417 user_env.locale, "de",
1418 "Should fallback to base locale if base is in available locales"
1419 );
1420 }
1421
1422 static ENGLISH_LOCALES: &[&str] = &["en-AU", "en-IE", "en-RU", "en-ZA"];
1423
1424 #[test]
1425 fn test_english_locales_fallbacks_to_en_us() {
1426 for user_locale in ENGLISH_LOCALES {
1427 let mut user_env = SearchUserEnvironment {
1428 locale: user_locale.to_string(),
1429 ..Default::default()
1430 };
1431 negotiate_languages(&mut user_env, &["en-US".to_string()]);
1432 assert_eq!(
1433 user_env.locale, "en-us",
1434 "Should remap {} to en-us when en-us is available",
1435 user_locale
1436 );
1437 }
1438 }
1439
1440 #[test]
1441 fn test_locale_unmatched() {
1442 let mut user_env = SearchUserEnvironment {
1443 locale: "fr-CA".into(),
1444 ..Default::default()
1445 };
1446 negotiate_languages(&mut user_env, &["de".to_string(), "en-US".to_string()]);
1447 assert_eq!(
1448 user_env.locale, "fr-CA",
1449 "Should leave locale unchanged if no match or english locale fallback is not found"
1450 );
1451 }
1452}