Skip to content

Commit 06a5d15

Browse files
committed
Bug 1923689 - Implement basic configuration processing on SearchEngineSelector.
This implements processing the main parts of the search configuration JSON into the Rust structure using serde_json. Currently the processing applies to: * The identifier and base properties of the engine records. * The default engine records. As variant and environment handling is not yet implemented, the filter function will return all engines defined in the configuration.
1 parent a486b51 commit 06a5d15

File tree

8 files changed

+633
-23
lines changed

8 files changed

+633
-23
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/search/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ license = "MPL-2.0"
88

99
[dependencies]
1010
error-support = { path = "../support/error" }
11+
parking_lot = ">=0.11,<=0.12"
12+
serde = { version = "1", features = ["derive"] }
13+
serde_json = "1"
1114
thiserror = "1"
1215
uniffi = { workspace = true }
1316

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 the structures that we use for serde_json to parse
6+
//! the search configuration.
7+
8+
use crate::{SearchEngineClassification, SearchUrlParam};
9+
use serde::Deserialize;
10+
11+
/// The list of possible submission methods for search engine urls.
12+
#[derive(Debug, uniffi::Enum, PartialEq, Deserialize, Clone, Default)]
13+
#[serde(rename_all = "UPPERCASE")]
14+
pub(crate) enum JSONEngineMethod {
15+
Post = 2,
16+
#[serde(other)]
17+
#[default]
18+
Get = 1,
19+
}
20+
21+
impl JSONEngineMethod {
22+
pub fn as_str(&self) -> &'static str {
23+
match self {
24+
JSONEngineMethod::Get => "GET",
25+
JSONEngineMethod::Post => "POST",
26+
}
27+
}
28+
}
29+
30+
/// Defines an individual search engine URL. This is defined separately to
31+
/// `types::SearchEngineUrl` as various fields may be optional in the supplied
32+
/// configuration.
33+
#[derive(Debug, uniffi::Record, PartialEq, Deserialize, Clone)]
34+
#[serde(rename_all = "camelCase")]
35+
pub(crate) struct JSONEngineUrl {
36+
/// The PrePath and FilePath of the URL. May include variables for engines
37+
/// which have a variable FilePath, e.g. `{searchTerm}` for when a search
38+
/// term is within the path of the url.
39+
pub base: String,
40+
41+
/// The HTTP method to use to send the request (`GET` or `POST`).
42+
/// If the engine definition has not specified the method, it defaults to GET.
43+
pub method: Option<JSONEngineMethod>,
44+
45+
/// The parameters for this URL.
46+
pub params: Option<Vec<SearchUrlParam>>,
47+
48+
/// The name of the query parameter for the search term. Automatically
49+
/// appended to the end of the query. This may be skipped if `{searchTerm}`
50+
/// is included in the base.
51+
pub search_term_param_name: Option<String>,
52+
}
53+
54+
/// Reflects `types::SearchEngineUrls`, but using `EngineUrl`.
55+
#[derive(Debug, uniffi::Record, PartialEq, Deserialize, Clone)]
56+
#[serde(rename_all = "camelCase")]
57+
pub(crate) struct JSONEngineUrls {
58+
/// The URL to use for searches.
59+
pub search: JSONEngineUrl,
60+
61+
/// The URL to use for suggestions.
62+
pub suggestions: Option<JSONEngineUrl>,
63+
64+
/// The URL to use for trending suggestions.
65+
pub trending: Option<JSONEngineUrl>,
66+
}
67+
68+
/// Represents the engine base section of the configuration.
69+
#[derive(Debug, Deserialize, Clone)]
70+
#[serde(rename_all = "camelCase")]
71+
pub(crate) struct JSONEngineBase {
72+
/// A list of aliases for this engine.
73+
pub aliases: Option<Vec<String>>,
74+
75+
/// The character set this engine uses for queries. Defaults to 'UTF=8' if not set.
76+
pub charset: Option<String>,
77+
78+
/// The classification of search engine according to the main search types
79+
/// (e.g. general, shopping, travel, dictionary). Currently, only marking as
80+
/// a general search engine is supported.
81+
pub classification: SearchEngineClassification,
82+
83+
/// The user visible name for the search engine.
84+
pub name: String,
85+
86+
/// The partner code for the engine. This will be inserted into parameters
87+
/// which include `{partnerCode}`.
88+
pub partner_code: Option<String>,
89+
90+
/// The URLs associated with the search engine.
91+
pub urls: JSONEngineUrls,
92+
}
93+
94+
/// Represents an individual engine record in the configuration.
95+
#[derive(Debug, Deserialize, Clone)]
96+
#[serde(rename_all = "camelCase")]
97+
pub(crate) struct JSONEngineRecord {
98+
pub identifier: String,
99+
pub base: JSONEngineBase,
100+
}
101+
102+
/// Represents the default engines record.
103+
#[derive(Debug, Deserialize, Clone)]
104+
#[serde(rename_all = "camelCase")]
105+
pub(crate) struct JSONDefaultEnginesRecord {
106+
pub global_default: String,
107+
pub global_default_private: Option<String>,
108+
}
109+
110+
/// Represents the engine orders record.
111+
#[derive(Debug, Deserialize, Clone)]
112+
#[serde(rename_all = "camelCase")]
113+
pub(crate) struct JSONEngineOrdersRecord {
114+
// TODO: Implementation.
115+
}
116+
117+
/// Represents an individual record in the raw search configuration.
118+
#[derive(Debug, Deserialize, Clone)]
119+
#[serde(tag = "recordType", rename_all = "camelCase")]
120+
pub(crate) enum JSONSearchConfigurationRecords {
121+
DefaultEngines(JSONDefaultEnginesRecord),
122+
Engine(Box<JSONEngineRecord>),
123+
EngineOrders(JSONEngineOrdersRecord),
124+
// Include some flexibilty if we choose to add new record types in future.
125+
// Current versions of the application receiving the configuration will
126+
// ignore the new record types.
127+
#[serde(other)]
128+
Unknown,
129+
}
130+
131+
/// Represents the search configuration as received from remote settings.
132+
#[derive(Debug, Deserialize)]
133+
pub(crate) struct JSONSearchConfiguration {
134+
pub data: Vec<JSONSearchConfigurationRecords>,
135+
}

components/search/src/error.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ use error_support::{ErrorHandling, GetErrorHandling};
1111
/// application.
1212
#[derive(Debug, thiserror::Error)]
1313
pub enum Error {
14-
#[error("NotImplemented")]
15-
NotImplemented,
14+
#[error("Search configuration not specified")]
15+
SearchConfigNotSpecified,
16+
#[error("JSON error: {0}")]
17+
Json(#[from] serde_json::Error),
1618
}
1719

1820
// #[non_exhaustive]

components/search/src/filter.rs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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+
use crate::{
6+
error::Error, JSONEngineBase, JSONEngineRecord, JSONEngineUrl, JSONEngineUrls,
7+
JSONSearchConfigurationRecords, RefinedSearchConfig, SearchEngineDefinition, SearchEngineUrl,
8+
SearchEngineUrls, SearchUserEnvironment,
9+
};
10+
11+
impl From<JSONEngineUrl> for SearchEngineUrl {
12+
fn from(url: JSONEngineUrl) -> Self {
13+
Self {
14+
base: url.base,
15+
method: url.method.unwrap_or_default().as_str().to_string(),
16+
params: url.params.unwrap_or_default(),
17+
search_term_param_name: url.search_term_param_name,
18+
}
19+
}
20+
}
21+
22+
impl From<JSONEngineUrls> for SearchEngineUrls {
23+
fn from(urls: JSONEngineUrls) -> Self {
24+
Self {
25+
search: urls.search.into(),
26+
suggestions: None,
27+
trending: None,
28+
}
29+
}
30+
}
31+
32+
impl SearchEngineDefinition {
33+
pub(crate) fn from_configuration_details(
34+
identifier: &str,
35+
base: JSONEngineBase,
36+
) -> SearchEngineDefinition {
37+
SearchEngineDefinition {
38+
aliases: base.aliases.unwrap_or_default(),
39+
charset: base.charset.unwrap_or_else(|| "UTF-8".to_string()),
40+
classification: base.classification,
41+
identifier: identifier.to_string(),
42+
name: base.name,
43+
order_hint: None,
44+
partner_code: base.partner_code.unwrap_or_default(),
45+
telemetry_suffix: None,
46+
urls: base.urls.into(),
47+
}
48+
}
49+
}
50+
51+
pub(crate) fn filter_engine_configuration(
52+
user_environment: SearchUserEnvironment,
53+
configuration: Vec<JSONSearchConfigurationRecords>,
54+
) -> Result<RefinedSearchConfig, Error> {
55+
let mut engines = Vec::new();
56+
let mut default_engine_id: Option<String> = None;
57+
let mut default_private_engine_id: Option<String> = None;
58+
59+
for record in configuration {
60+
match record {
61+
JSONSearchConfigurationRecords::Engine(engine) => {
62+
let result = extract_engine_config(&user_environment, engine);
63+
engines.extend(result);
64+
}
65+
JSONSearchConfigurationRecords::DefaultEngines(default_engines) => {
66+
default_engine_id = Some(default_engines.global_default);
67+
default_private_engine_id.clone_from(&default_engines.global_default_private);
68+
}
69+
JSONSearchConfigurationRecords::EngineOrders(_engine_orders) => {
70+
// TODO: Implementation.
71+
}
72+
JSONSearchConfigurationRecords::Unknown => {
73+
// Prevents panics if a new record type is added in future.
74+
}
75+
}
76+
}
77+
78+
Ok(RefinedSearchConfig {
79+
engines,
80+
app_default_engine_id: default_engine_id.unwrap(),
81+
app_default_private_engine_id: default_private_engine_id,
82+
})
83+
}
84+
85+
fn extract_engine_config(
86+
_user_environment: &SearchUserEnvironment,
87+
record: Box<JSONEngineRecord>,
88+
) -> Option<SearchEngineDefinition> {
89+
// TODO: Variant handling.
90+
Some(SearchEngineDefinition::from_configuration_details(
91+
&record.identifier,
92+
record.base,
93+
))
94+
}
95+
96+
#[cfg(test)]
97+
mod tests {
98+
use super::*;
99+
use crate::types::*;
100+
101+
#[test]
102+
fn test_from_configuration_details_fallsback_to_defaults() {
103+
let result = SearchEngineDefinition::from_configuration_details(
104+
"test",
105+
JSONEngineBase {
106+
aliases: None,
107+
charset: None,
108+
classification: SearchEngineClassification::General,
109+
name: "Test".to_string(),
110+
partner_code: None,
111+
urls: JSONEngineUrls {
112+
search: JSONEngineUrl {
113+
base: "https://example.com".to_string(),
114+
method: None,
115+
params: None,
116+
search_term_param_name: None,
117+
},
118+
suggestions: None,
119+
trending: None,
120+
},
121+
},
122+
);
123+
124+
assert_eq!(
125+
result,
126+
SearchEngineDefinition {
127+
aliases: Vec::new(),
128+
charset: "UTF-8".to_string(),
129+
classification: SearchEngineClassification::General,
130+
identifier: "test".to_string(),
131+
partner_code: String::new(),
132+
name: "Test".to_string(),
133+
order_hint: None,
134+
telemetry_suffix: None,
135+
urls: SearchEngineUrls {
136+
search: SearchEngineUrl {
137+
base: "https://example.com".to_string(),
138+
method: "GET".to_string(),
139+
params: Vec::new(),
140+
search_term_param_name: None,
141+
},
142+
suggestions: None,
143+
trending: None
144+
}
145+
}
146+
)
147+
}
148+
149+
#[test]
150+
fn test_from_configuration_details_uses_values() {
151+
let result = SearchEngineDefinition::from_configuration_details(
152+
"test",
153+
JSONEngineBase {
154+
aliases: Some(vec!["foo".to_string(), "bar".to_string()]),
155+
charset: Some("ISO-8859-15".to_string()),
156+
classification: SearchEngineClassification::Unknown,
157+
name: "Test".to_string(),
158+
partner_code: Some("firefox".to_string()),
159+
urls: JSONEngineUrls {
160+
search: JSONEngineUrl {
161+
base: "https://example.com".to_string(),
162+
method: Some(crate::JSONEngineMethod::Post),
163+
params: Some(vec![SearchUrlParam {
164+
name: "param".to_string(),
165+
value: Some("test param".to_string()),
166+
experiment_config: None,
167+
}]),
168+
search_term_param_name: Some("baz".to_string()),
169+
},
170+
suggestions: None,
171+
trending: None,
172+
},
173+
},
174+
);
175+
176+
assert_eq!(
177+
result,
178+
SearchEngineDefinition {
179+
aliases: vec!["foo".to_string(), "bar".to_string()],
180+
charset: "ISO-8859-15".to_string(),
181+
classification: SearchEngineClassification::Unknown,
182+
identifier: "test".to_string(),
183+
partner_code: "firefox".to_string(),
184+
name: "Test".to_string(),
185+
order_hint: None,
186+
telemetry_suffix: None,
187+
urls: SearchEngineUrls {
188+
search: SearchEngineUrl {
189+
base: "https://example.com".to_string(),
190+
method: "POST".to_string(),
191+
params: vec![SearchUrlParam {
192+
name: "param".to_string(),
193+
value: Some("test param".to_string()),
194+
experiment_config: None,
195+
}],
196+
search_term_param_name: Some("baz".to_string()),
197+
},
198+
suggestions: None,
199+
trending: None
200+
}
201+
}
202+
)
203+
}
204+
}

components/search/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
mod configuration_types;
56
mod error;
7+
mod filter;
68
pub use error::SearchApiError;
79

810
pub mod selector;
911
pub mod types;
1012

13+
pub(crate) use crate::configuration_types::*;
1114
pub use crate::types::*;
1215
pub use selector::SearchEngineSelector;
1316
pub type SearchApiResult<T> = std::result::Result<T, error::SearchApiError>;

0 commit comments

Comments
 (0)