From 64c9b28ad2d38c242a00f8d412be071a2dcded7b Mon Sep 17 00:00:00 2001 From: Tif Tran Date: Wed, 5 Aug 2026 17:46:07 -0700 Subject: [PATCH] [DISCO-4371] Merino: remove wcs --- components/merino/src/lib.rs | 1 - components/merino/src/worldcup/error.rs | 76 ---- .../merino/src/worldcup/fixtures/live.json | 60 --- .../merino/src/worldcup/fixtures/matches.json | 346 ------------------ .../merino/src/worldcup/fixtures/teams.json | 23 -- components/merino/src/worldcup/http.rs | 149 -------- components/merino/src/worldcup/mod.rs | 140 ------- components/merino/src/worldcup/schema.rs | 20 - components/merino/src/worldcup/tests.rs | 273 -------------- examples/merino-cli/src/main.rs | 60 --- 10 files changed, 1148 deletions(-) delete mode 100644 components/merino/src/worldcup/error.rs delete mode 100644 components/merino/src/worldcup/fixtures/live.json delete mode 100644 components/merino/src/worldcup/fixtures/matches.json delete mode 100644 components/merino/src/worldcup/fixtures/teams.json delete mode 100644 components/merino/src/worldcup/http.rs delete mode 100644 components/merino/src/worldcup/mod.rs delete mode 100644 components/merino/src/worldcup/schema.rs delete mode 100644 components/merino/src/worldcup/tests.rs diff --git a/components/merino/src/lib.rs b/components/merino/src/lib.rs index 8dd0504dcf0..eab0d9d1518 100644 --- a/components/merino/src/lib.rs +++ b/components/merino/src/lib.rs @@ -20,6 +20,5 @@ pub mod curated_recommendations; pub mod suggest; -pub mod worldcup; pub use curated_recommendations::{CuratedRecommendationLocale, CuratedRecommendationsApiError}; uniffi::setup_scaffolding!("merino"); diff --git a/components/merino/src/worldcup/error.rs b/components/merino/src/worldcup/error.rs deleted file mode 100644 index d1c36cb362e..00000000000 --- a/components/merino/src/worldcup/error.rs +++ /dev/null @@ -1,76 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use error_support::{ErrorHandling, GetErrorHandling}; - -pub type Result = std::result::Result; -pub type ApiResult = std::result::Result; - -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum MerinoWorldCupApiError { - /// A network-level failure. - #[error("WorldCup network error: {reason}")] - Network { reason: String }, - - /// Any other error, e.g. HTTP errors, validation errors. - #[error("WorldCup error: code {code:?}, reason: {reason}")] - Other { code: Option, reason: String }, -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - /// Failed to parse a URL. - #[error("URL parse error: {0}")] - UrlParse(#[from] url::ParseError), - - /// Failed to send the HTTP request. - #[error("Error sending request: {0}")] - Request(#[from] viaduct::ViaductError), - - /// The server rejected the request due to malformed syntax (HTTP 400). - #[error("Bad request ({code}): {message}")] - BadRequest { code: u16, message: String }, - - /// The server rejected the request due to invalid input (HTTP 422). - #[error("Validation error ({code}): {message}")] - Validation { code: u16, message: String }, - - /// The server encountered an internal error (HTTP 5xx). - #[error("Server error ({code}): {message}")] - Server { code: u16, message: String }, - - /// An unexpected HTTP status code was received. - #[error("Unexpected error ({code}): {message}")] - Unexpected { code: u16, message: String }, -} - -impl GetErrorHandling for Error { - type ExternalError = MerinoWorldCupApiError; - - fn get_error_handling(&self) -> ErrorHandling { - match self { - Self::Request { .. } => ErrorHandling::convert(MerinoWorldCupApiError::Network { - reason: self.to_string(), - }) - .log_warning(), - - Self::Validation { code, .. } - | Self::Server { code, .. } - | Self::Unexpected { code, .. } - | Self::BadRequest { code, .. } => { - ErrorHandling::convert(MerinoWorldCupApiError::Other { - code: Some(*code), - reason: self.to_string(), - }) - .report_error("merino-http-error") - } - - Self::UrlParse(_) => ErrorHandling::convert(MerinoWorldCupApiError::Other { - code: None, - reason: self.to_string(), - }) - .report_error("merino-unexpected"), - } - } -} diff --git a/components/merino/src/worldcup/fixtures/live.json b/components/merino/src/worldcup/fixtures/live.json deleted file mode 100644 index b176483375a..00000000000 --- a/components/merino/src/worldcup/fixtures/live.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "current": [ - { - "date": "2026-04-30T14:00:00+00:00", - "global_event_id": 1002, - "home_team": { - "key": "ENG", - "global_team_id": 90000005, - "name": "England", - "region": "ENG", - "colors": [ - "White", - "Red" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_gb-eng.png", - "group": "Group C", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "away_team": { - "key": "USA", - "global_team_id": 90000006, - "name": "United States", - "region": "USA", - "colors": [ - "Navy", - "White", - "Red" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_us.png", - "group": "Group C", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "period": "2", - "home_score": 1, - "away_score": 0, - "home_extra": null, - "away_extra": null, - "home_penalty": null, - "away_penalty": null, - "clock": "67", - "updated": 1777554000, - "status": "In Progress", - "status_type": "live", - "query": null, - "sport": "soccer" - } - ] -} \ No newline at end of file diff --git a/components/merino/src/worldcup/fixtures/matches.json b/components/merino/src/worldcup/fixtures/matches.json deleted file mode 100644 index fd75eb07b53..00000000000 --- a/components/merino/src/worldcup/fixtures/matches.json +++ /dev/null @@ -1,346 +0,0 @@ -{ - "previous": [ - { - "date": "2026-04-29T14:00:00+00:00", - "global_event_id": 1000, - "home_team": { - "key": "BRA", - "global_team_id": 90000001, - "name": "Brazil", - "region": "BRA", - "colors": [ - "Yellow", - "Green", - "Blue" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_br.png", - "group": "Group A", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "away_team": { - "key": "ARG", - "global_team_id": 90000002, - "name": "Argentina", - "region": "ARG", - "colors": [ - "Sky Blue", - "White" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_ar.png", - "group": "Group A", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "period": "FT", - "home_score": 2, - "away_score": 1, - "home_extra": null, - "away_extra": null, - "home_penalty": null, - "away_penalty": null, - "clock": "90", - "updated": 1777467600, - "status": "Final", - "status_type": "past", - "query": null, - "sport": "soccer" - }, - { - "date": "2026-04-29T18:00:00+00:00", - "global_event_id": 1001, - "home_team": { - "key": "GER", - "global_team_id": 90000003, - "name": "Germany", - "region": "GER", - "colors": [ - "Black", - "Red", - "Yellow" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_de.png", - "group": "Group B", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "away_team": { - "key": "FRA", - "global_team_id": 90000004, - "name": "France", - "region": "FRA", - "colors": [ - "Blue", - "White", - "Red" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_fr.png", - "group": "Group B", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "period": "FT(P)", - "home_score": 1, - "away_score": 1, - "home_extra": 1, - "away_extra": 1, - "home_penalty": 5, - "away_penalty": 4, - "clock": "120", - "updated": 1777482000, - "status": "Final", - "status_type": "past", - "query": null, - "sport": "soccer" - } - ], - "current": [ - { - "date": "2026-04-30T14:00:00+00:00", - "global_event_id": 1002, - "home_team": { - "key": "ENG", - "global_team_id": 90000005, - "name": "England", - "region": "ENG", - "colors": [ - "White", - "Red" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_gb-eng.png", - "group": "Group C", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "away_team": { - "key": "USA", - "global_team_id": 90000006, - "name": "United States", - "region": "USA", - "colors": [ - "Navy", - "White", - "Red" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_us.png", - "group": "Group C", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "period": "2", - "home_score": 1, - "away_score": 0, - "home_extra": null, - "away_extra": null, - "home_penalty": null, - "away_penalty": null, - "clock": "67", - "updated": 1777554000, - "status": "In Progress", - "status_type": "live", - "query": null, - "sport": "soccer" - }, - { - "date": "2026-04-30T17:00:00+00:00", - "global_event_id": 1003, - "home_team": { - "key": "BRA", - "global_team_id": 90000001, - "name": "Brazil", - "region": "BRA", - "colors": [ - "Yellow", - "Green", - "Blue" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_br.png", - "group": "Group A", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "away_team": { - "key": "GER", - "global_team_id": 90000003, - "name": "Germany", - "region": "GER", - "colors": [ - "Black", - "Red", - "Yellow" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_de.png", - "group": "Group B", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "period": "ET", - "home_score": 2, - "away_score": 2, - "home_extra": null, - "away_extra": null, - "home_penalty": null, - "away_penalty": null, - "clock": "90+15", - "updated": 1777564800, - "status": "In Progress", - "status_type": "live", - "query": null, - "sport": "soccer" - } - ], - "next": [ - { - "date": "2026-05-01T15:00:00+00:00", - "global_event_id": 1004, - "home_team": { - "key": "ARG", - "global_team_id": 90000002, - "name": "Argentina", - "region": "ARG", - "colors": [ - "Sky Blue", - "White" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_ar.png", - "group": "Group A", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "away_team": { - "key": "ENG", - "global_team_id": 90000005, - "name": "England", - "region": "ENG", - "colors": [ - "White", - "Red" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_gb-eng.png", - "group": "Group C", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "period": "1", - "home_score": null, - "away_score": null, - "home_extra": null, - "away_extra": null, - "home_penalty": null, - "away_penalty": null, - "clock": "0", - "updated": 1777644000, - "status": "Scheduled", - "status_type": "scheduled", - "query": null, - "sport": "soccer" - }, - { - "date": "2026-05-01T19:00:00+00:00", - "global_event_id": 1005, - "home_team": { - "key": "FRA", - "global_team_id": 90000004, - "name": "France", - "region": "FRA", - "colors": [ - "Blue", - "White", - "Red" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_fr.png", - "group": "Group B", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "away_team": { - "key": "USA", - "global_team_id": 90000006, - "name": "United States", - "region": "USA", - "colors": [ - "Navy", - "White", - "Red" - ], - "icon_url": "https://storage.googleapis.com/merino-images-prod/logos/nations/nations_us.png", - "group": "Group C", - "eliminated": false, - "standing": { - "wins": 0, - "losses": 0, - "draws": 0, - "points": 0 - } - }, - "period": "1", - "home_score": null, - "away_score": null, - "home_extra": null, - "away_extra": null, - "home_penalty": null, - "away_penalty": null, - "clock": "0", - "updated": 1777658400, - "status": "Scheduled", - "status_type": "scheduled", - "query": null, - "sport": "soccer" - } - ] -} \ No newline at end of file diff --git a/components/merino/src/worldcup/fixtures/teams.json b/components/merino/src/worldcup/fixtures/teams.json deleted file mode 100644 index 39c1721737e..00000000000 --- a/components/merino/src/worldcup/fixtures/teams.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "teams": [ - { - "key": "ENG", - "global_team_id": 1001, - "name": "England", - "region": "ENG", - "colors": ["White", "Red", "Navy Blue"], - "icon": "https://example.com/logos/england.png", - "group": "Group A", - "eliminated": false - }, - { - "key": "BRA", - "global_team_id": 1002, - "name": "Brazil", - "region": "BRA", - "colors": ["Yellow", "Green", "Blue"], - "icon": "https://example.com/logos/brazil.png", - "group": "Group A", - "eliminated": false - }] -} \ No newline at end of file diff --git a/components/merino/src/worldcup/http.rs b/components/merino/src/worldcup/http.rs deleted file mode 100644 index b16de88b10e..00000000000 --- a/components/merino/src/worldcup/http.rs +++ /dev/null @@ -1,149 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use url::Url; -use viaduct::{Request, Response}; - -use super::error::{Error, Result}; - -pub struct HttpClient; - -#[derive(Default)] -pub struct WorldCupQueryParams { - pub limit: Option, - pub teams: Option, - pub accept_language: Option, - pub date: Option, -} - -pub trait HttpClientTrait { - fn make_request(&self, url: Url, params: WorldCupQueryParams) -> Result>; -} - -impl HttpClientTrait for HttpClient { - fn make_request(&self, url: Url, params: WorldCupQueryParams) -> Result> { - send_get(build_url(url, ¶ms), params.accept_language) - } -} - -pub fn build_url(endpoint_url: Url, params: &WorldCupQueryParams) -> Url { - if params.limit.is_none() && params.teams.is_none() && params.date.is_none() { - return endpoint_url; - } - let mut url = endpoint_url; - { - let mut pairs = url.query_pairs_mut(); - if let Some(v) = params.limit { - pairs.append_pair("limit", &v.to_string()); - } - if let Some(v) = ¶ms.teams { - pairs.append_pair("teams", v); - } - if let Some(v) = ¶ms.date { - pairs.append_pair("date", v); - } - } - url -} - -fn send_get(url: Url, accept_language: Option) -> Result> { - let mut request = Request::get(url).header("accept", "application/json")?; - if let Some(lang) = accept_language { - request = request.header("accept-language", lang)?; - } - let response = request.send()?; - let status = response.status; - match status { - 200 => Ok(Some(response)), - 204 => Ok(None), - 400 => Err(Error::BadRequest { - code: status, - message: response.text().to_string(), - }), - 422 => Err(Error::Validation { - code: status, - message: response.text().to_string(), - }), - 500..=599 => Err(Error::Server { - code: status, - message: response.text().to_string(), - }), - _ => Err(Error::Unexpected { - code: status, - message: response.text().to_string(), - }), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const BASE_URL: &str = "https://merino.services.mozilla.com/api/v1/wcs/teams"; - - fn base_url() -> Url { - Url::parse(BASE_URL).unwrap() - } - - fn has_param(url: &Url, key: &str, value: &str) -> bool { - url.query_pairs().any(|(k, v)| k == key && v == value) - } - - #[test] - fn test_build_url_with_params() { - let options = WorldCupQueryParams { - limit: Some(10), - ..WorldCupQueryParams::default() - }; - let url = build_url(base_url(), &options); - assert!(has_param(&url, "limit", "10")); - } - - #[test] - fn test_build_url_with_teams() { - let options = WorldCupQueryParams { - teams: Some("FRA,ENG".to_string()), - ..WorldCupQueryParams::default() - }; - let url = build_url(base_url(), &options); - assert!(has_param(&url, "teams", "FRA,ENG")); - } - - #[test] - fn test_build_url_with_all_options() { - let options = WorldCupQueryParams { - limit: Some(5), - teams: Some("FRA,ENG".to_string()), - accept_language: Some("en-GB".to_string()), - date: None, - }; - let url = build_url(base_url(), &options); - assert!(has_param(&url, "limit", "5")); - assert!(has_param(&url, "teams", "FRA,ENG")); - } - - #[test] - fn test_build_url_omits_none_options() { - let url = build_url(base_url(), &WorldCupQueryParams::default()); - let keys: Vec<_> = url.query_pairs().map(|(k, _)| k.into_owned()).collect(); - assert_eq!(keys.len(), 0); - } - - #[test] - fn test_build_url_full_string() { - let options = WorldCupQueryParams { - limit: Some(3), - teams: Some("FRA".to_string()), - accept_language: Some("en-US".to_string()), - date: None, - }; - let url = build_url(base_url(), &options); - assert_eq!( - url.to_string(), - "https://merino.services.mozilla.com/api/v1/wcs/teams\ - ?limit=3\ - &teams=FRA" - ); - } -} diff --git a/components/merino/src/worldcup/mod.rs b/components/merino/src/worldcup/mod.rs deleted file mode 100644 index 9c80ff2c11d..00000000000 --- a/components/merino/src/worldcup/mod.rs +++ /dev/null @@ -1,140 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -mod error; -mod http; -mod schema; -#[cfg(test)] -mod tests; - -use error_support::handle_error; -use url::Url; - -pub use error::{ApiResult, Error, MerinoWorldCupApiError, Result}; -pub use schema::{WorldCupConfig, WorldCupOptions}; - -pub(crate) const DEFAULT_BASE_URL: &str = "https://merino.services.mozilla.com/api/v1/wcs/"; - -/// A client for the merino wcs endpoint. -/// -/// Use [`WorldCupClient::new`] to create an instance, then call -/// [`WordCupClient::get_*`] to fetch wcs content. -#[derive(uniffi::Object)] -pub struct WorldCupClient { - inner: WorldCupClientInner, - base_url: Url, -} - -struct WorldCupClientInner { - http_client: T, -} - -#[derive(Default)] -struct WorldCupClientBuilder { - base_host: Option, -} - -impl WorldCupClientBuilder { - pub fn new() -> Self { - Self::default() - } - - pub fn base_host(mut self, base_host: String) -> Self { - self.base_host = Some(base_host); - self - } - - pub fn build(self) -> Result { - let base_url = match self.base_host { - Some(host) => Url::parse(&format!("{}/api/v1/wcs/", host))?, - None => Url::parse(DEFAULT_BASE_URL)?, - }; - - Ok(WorldCupClient { - inner: WorldCupClientInner::new()?, - base_url, - }) - } -} - -#[uniffi::export] -impl WorldCupClient { - /// Creates a new `WorldCupClient` from the given configuration. - #[uniffi::constructor] - #[handle_error(Error)] - pub fn new(config: WorldCupConfig) -> ApiResult { - let mut builder = WorldCupClientBuilder::new(); - - if let Some(host) = config.base_host { - builder = builder.base_host(host); - } - - builder.build() - } - - #[handle_error(Error)] - /// Fetches teams from the merino wcs endpoint - pub fn get_teams(&self, options: WorldCupOptions) -> ApiResult> { - let url = self.base_url.join("teams")?; - let response = self.inner.make_request(url, options)?; - Ok(response.map(|r| r.text().to_string())) - } - - #[handle_error(Error)] - /// Fetches matches from merino wcs endpoint - pub fn get_matches(&self, options: WorldCupOptions) -> ApiResult> { - let url = self.base_url.join("matches")?; - let response = self.inner.make_request(url, options)?; - Ok(response.map(|r| r.text().to_string())) - } - - #[handle_error(Error)] - /// Fetches live info from merino wcs endpoint - pub fn get_live(&self, options: WorldCupOptions) -> ApiResult> { - let url = self.base_url.join("live")?; - let response = self.inner.make_request(url, options)?; - Ok(response.map(|r| r.text().to_string())) - } -} - -impl WorldCupClientInner { - pub fn new() -> Result { - Ok(Self { - http_client: http::HttpClient, - }) - } -} - -impl WorldCupClientInner { - fn params(options: WorldCupOptions) -> http::WorldCupQueryParams { - let teams = options - .teams - .as_ref() - .filter(|v| !v.is_empty()) - .map(|v| v.join(",")); - http::WorldCupQueryParams { - limit: options.limit, - teams, - accept_language: options.accept_language, - date: options.date, - } - } - - pub fn make_request( - &self, - url: Url, - options: WorldCupOptions, - ) -> Result> { - self.http_client.make_request(url, Self::params(options)) - } -} - -#[cfg(test)] -impl WorldCupClientInner { - pub fn new_with_client(client: T) -> Self { - Self { - http_client: client, - } - } -} diff --git a/components/merino/src/worldcup/schema.rs b/components/merino/src/worldcup/schema.rs deleted file mode 100644 index 70e32c6149c..00000000000 --- a/components/merino/src/worldcup/schema.rs +++ /dev/null @@ -1,20 +0,0 @@ -use uniffi::Record; - -#[derive(Clone, Debug, Record)] -pub struct WorldCupConfig { - pub base_host: Option, -} - -/// Options for world cup endpoint requests. -/// All fields are optional — omitted fields are not sent to merino. -#[derive(Clone, Debug, Record)] -pub struct WorldCupOptions { - /// Maximum number of results to return. - pub limit: Option, - /// Filter results by team(s) (e.g. `["FRA", "ENG"]`). - pub teams: Option>, - /// Language for results (e.g. `"en-US"`). (Not supported yet) - pub accept_language: Option, - /// ISO 8601 date string to filter matches by date (e.g. `"2026-06-14"`). - pub date: Option, -} diff --git a/components/merino/src/worldcup/tests.rs b/components/merino/src/worldcup/tests.rs deleted file mode 100644 index 616e9a93445..00000000000 --- a/components/merino/src/worldcup/tests.rs +++ /dev/null @@ -1,273 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use super::*; -use url::Url; -use viaduct::{Headers, Method, Response}; - -const TEAMS_RESPONSE: &str = include_str!("fixtures/teams.json"); -const MATCH_RESPONSE: &str = include_str!("fixtures/matches.json"); -const LIVE_RESPONSE: &str = include_str!("fixtures/live.json"); - -fn make_response(status: u16, body: &str, url: Url, accept_language: Option) -> Response { - let mut headers = Headers::new(); - if let Some(lang) = accept_language { - headers.insert("accept-language", lang).unwrap(); - } - Response { - request_method: Method::Get, - url, - status, - headers, - body: body.as_bytes().to_vec(), - } -} - -fn default_options() -> WorldCupOptions { - WorldCupOptions { - limit: None, - teams: None, - accept_language: None, - date: None, - } -} - -fn base_url() -> Url { - Url::parse(DEFAULT_BASE_URL).unwrap() -} - -struct FakeHttpClient(fn(http::WorldCupQueryParams) -> Result>); - -impl http::HttpClientTrait for FakeHttpClient { - fn make_request( - &self, - _url: Url, - params: http::WorldCupQueryParams, - ) -> Result> { - (self.0)(params) - } -} - -struct FakeCapturingClient { - captured_url: std::sync::Arc>>, -} - -impl http::HttpClientTrait for FakeCapturingClient { - fn make_request( - &self, - url: Url, - params: http::WorldCupQueryParams, - ) -> Result> { - *self.captured_url.lock().unwrap() = Some(http::build_url(url.clone(), ¶ms)); - Ok(Some(make_response(200, "{}", url, None))) - } -} - -#[test] -fn test_get_teams_success() { - fn response(_params: http::WorldCupQueryParams) -> Result> { - Ok(Some(make_response( - 200, - TEAMS_RESPONSE, - Url::parse("https://merino.services.mozilla.com/api/v1/wcs/").unwrap(), - None, - ))) - } - let client = WorldCupClientInner::new_with_client(FakeHttpClient(response)); - let result = client.make_request(base_url().join("teams").unwrap(), default_options()); - assert!(result.is_ok()); - assert_eq!(result.unwrap().unwrap().text(), TEAMS_RESPONSE); -} - -#[test] -fn test_get_matches_success() { - fn response(_params: http::WorldCupQueryParams) -> Result> { - Ok(Some(make_response( - 200, - MATCH_RESPONSE, - Url::parse("https://merino.services.mozilla.com/api/v1/wcs/").unwrap(), - None, - ))) - } - let client = WorldCupClientInner::new_with_client(FakeHttpClient(response)); - let result = client.make_request(base_url().join("matches").unwrap(), default_options()); - assert!(result.is_ok()); - assert_eq!(result.unwrap().unwrap().text(), MATCH_RESPONSE); -} - -#[test] -fn test_get_live_success() { - fn response(_params: http::WorldCupQueryParams) -> Result> { - Ok(Some(make_response( - 200, - LIVE_RESPONSE, - Url::parse("https://merino.services.mozilla.com/api/v1/wcs/").unwrap(), - None, - ))) - } - let client = WorldCupClientInner::new_with_client(FakeHttpClient(response)); - let result = client.make_request(base_url().join("live").unwrap(), default_options()); - assert!(result.is_ok()); - assert_eq!(result.unwrap().unwrap().text(), LIVE_RESPONSE); -} - -#[test] -fn test_no_content_returns_none() { - fn response(_params: http::WorldCupQueryParams) -> Result> { - Ok(None) - } - let client = WorldCupClientInner::new_with_client(FakeHttpClient(response)); - - let result = client.make_request(base_url().join("teams").unwrap(), default_options()); - assert!(result.unwrap().is_none()); - - let result = client.make_request(base_url().join("matches").unwrap(), default_options()); - assert!(result.unwrap().is_none()); - - let result = client.make_request(base_url().join("live").unwrap(), default_options()); - assert!(result.unwrap().is_none()); -} - -#[test] -fn test_server_error() { - fn response(_params: http::WorldCupQueryParams) -> Result> { - Err(Error::Server { - code: 500, - message: "Internal server error".to_string(), - }) - } - let client = WorldCupClientInner::new_with_client(FakeHttpClient(response)); - let result = client.make_request(base_url().join("teams").unwrap(), default_options()); - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - Error::Server { code: 500, .. } - )); -} - -#[test] -fn test_accept_language_is_passed_as_header() { - fn response(params: http::WorldCupQueryParams) -> Result> { - Ok(Some(make_response( - 200, - "", - Url::parse("https://merino.services.mozilla.com/api/v1/wcs/").unwrap(), - params.accept_language, - ))) - } - let client = WorldCupClientInner::new_with_client(FakeHttpClient(response)); - let result = client.make_request( - base_url().join("teams").unwrap(), - WorldCupOptions { - limit: None, - teams: None, - accept_language: Some("en-US".to_string()), - date: None, - }, - ); - let response = result.unwrap().unwrap(); - assert_eq!(response.headers.get("accept-language"), Some("en-US")); -} - -#[test] -fn test_builder_uses_default_base_host() { - let client = WorldCupClientBuilder::new().build().unwrap(); - assert_eq!(client.base_url.as_str(), DEFAULT_BASE_URL); -} - -#[test] -fn test_builder_uses_custom_base_host() { - let client = WorldCupClientBuilder::new() - .base_host("https://stage.merino.services.mozilla.com".to_string()) - .build() - .unwrap(); - assert_eq!( - client.base_url.as_str(), - "https://stage.merino.services.mozilla.com/api/v1/wcs/" - ); -} - -#[test] -fn test_teams_endpoint_url() { - let captured_url = std::sync::Arc::new(std::sync::Mutex::new(None::)); - let client_inner = WorldCupClientInner::new_with_client(FakeCapturingClient { - captured_url: captured_url.clone(), - }); - let _ = client_inner.make_request(base_url().join("teams").unwrap(), default_options()); - let captured = captured_url.lock().unwrap(); - assert_eq!( - captured.as_ref().unwrap().as_str(), - "https://merino.services.mozilla.com/api/v1/wcs/teams" - ); -} - -#[test] -fn test_matches_endpoint_url_with_limit() { - let captured_url = std::sync::Arc::new(std::sync::Mutex::new(None::)); - let client_inner = WorldCupClientInner::new_with_client(FakeCapturingClient { - captured_url: captured_url.clone(), - }); - let _ = client_inner.make_request( - base_url().join("matches").unwrap(), - WorldCupOptions { - limit: Some(2), - teams: None, - accept_language: None, - date: None, - }, - ); - let captured = captured_url.lock().unwrap(); - assert_eq!( - captured.as_ref().unwrap().as_str(), - "https://merino.services.mozilla.com/api/v1/wcs/matches?limit=2" - ); -} - -#[test] -fn test_live_endpoint_url() { - let captured_url = std::sync::Arc::new(std::sync::Mutex::new(None::)); - let client_inner = WorldCupClientInner::new_with_client(FakeCapturingClient { - captured_url: captured_url.clone(), - }); - let _ = client_inner.make_request(base_url().join("live").unwrap(), default_options()); - let captured = captured_url.lock().unwrap(); - assert_eq!( - captured.as_ref().unwrap().as_str(), - "https://merino.services.mozilla.com/api/v1/wcs/live" - ); -} - -#[test] -fn test_builder_fails_with_invalid_base_host() { - let result = WorldCupClientBuilder::new() - .base_host("not a valid url".to_string()) - .build(); - match result { - Err(Error::UrlParse(_)) => {} - Err(other) => panic!("Expected UrlParse error, got: {:?}", other), - Ok(_) => panic!("Expected error for invalid base_host"), - } -} - -#[test] -fn test_matches_endpoint_url_with_date() { - let captured_url = std::sync::Arc::new(std::sync::Mutex::new(None::)); - let client_inner = WorldCupClientInner::new_with_client(FakeCapturingClient { - captured_url: captured_url.clone(), - }); - let _ = client_inner.make_request( - base_url().join("matches").unwrap(), - WorldCupOptions { - limit: None, - teams: None, - accept_language: None, - date: Some("2026-06-14".to_string()), - }, - ); - let captured = captured_url.lock().unwrap(); - assert_eq!( - captured.as_ref().unwrap().as_str(), - "https://merino.services.mozilla.com/api/v1/wcs/matches?date=2026-06-14" - ); -} diff --git a/examples/merino-cli/src/main.rs b/examples/merino-cli/src/main.rs index 316ed83b952..1f4e8633160 100644 --- a/examples/merino-cli/src/main.rs +++ b/examples/merino-cli/src/main.rs @@ -11,7 +11,6 @@ use merino::curated_recommendations::models::request::{ }; use merino::curated_recommendations::CuratedRecommendationsClient; use merino::suggest::{SuggestClient, SuggestConfig, SuggestOptions}; -use merino::worldcup::{WorldCupClient, WorldCupConfig, WorldCupOptions}; use viaduct::{configure_ohttp_channel, OhttpConfig}; #[derive(Debug, Parser)] @@ -86,36 +85,6 @@ enum Commands { #[arg(long)] accept_language: Option, }, - /// Fetch World Cup data - WorldCup { - /// Maximum number of results to return - #[arg(long)] - limit: Option, - - /// Filter by team codes (e.g. --teams FRA --teams ENG) - #[arg(long)] - teams: Option>, - - /// Language for results (e.g. "en-US") - #[arg(long)] - accept_language: Option, - /// Date to filter matches by, in `YYYY-MM-DD` format (e.g. "2026-06-14") - #[arg(long)] - date: Option, - - #[command(subcommand)] - endpoint: WorldCupEndpoint, - }, -} - -#[derive(Debug, Subcommand)] -enum WorldCupEndpoint { - /// Fetch teams - Teams, - /// Fetch matches - Matches, - /// Fetch live match data - Live, } fn main() -> Result<()> { @@ -185,35 +154,6 @@ fn main() -> Result<()> { None => println!("No suggestions available (204 No Content)"), } } - Commands::WorldCup { - limit, - teams, - accept_language, - date, - endpoint, - } => { - let client = WorldCupClient::new(WorldCupConfig { - base_host: cli.base_host, - })?; - let options = WorldCupOptions { - limit, - teams, - accept_language, - date, - }; - let result = match endpoint { - WorldCupEndpoint::Teams => client.get_teams(options), - WorldCupEndpoint::Matches => client.get_matches(options), - WorldCupEndpoint::Live => client.get_live(options), - }; - match result? { - Some(response) => { - let json: serde_json::Value = serde_json::from_str(&response)?; - println!("{}", serde_json::to_string_pretty(&json)?); - } - None => println!("No data available (204 No Content)"), - } - } } Ok(())