use super::{GameFamilies, GameFamily, ItemType};
use crate::{BoardGameGeekApi, Error, IntoQueryParam, QueryParam, Result};
#[derive(Clone, Debug, Default)]
struct GameFamilyQueryParams {
game_family_ids: Vec<u64>,
}
impl GameFamilyQueryParams {
fn new() -> Self {
Self::default()
}
fn game_family_id(mut self, id: u64) -> Self {
self.game_family_ids.push(id);
self
}
fn game_family_ids(mut self, ids: Vec<u64>) -> Self {
self.game_family_ids.extend(ids);
self
}
}
#[derive(Clone, Debug)]
struct GameFamilyQueryBuilder {
params: GameFamilyQueryParams,
}
impl<'builder> GameFamilyQueryBuilder {
fn new(params: GameFamilyQueryParams) -> Self {
Self { params }
}
fn build(self) -> Vec<QueryParam<'builder>> {
let mut query_params: Vec<_> = vec![];
query_params.push(ItemType::BoardGameFamily.into_query_param("type"));
if !self.params.game_family_ids.is_empty() {
query_params.push(self.params.game_family_ids.into_query_param("id"));
}
query_params
}
}
pub struct GameFamilyApi<'api> {
pub(crate) api: &'api BoardGameGeekApi,
endpoint: &'static str,
}
impl<'api> GameFamilyApi<'api> {
pub(crate) fn new(api: &'api BoardGameGeekApi) -> Self {
Self {
api,
endpoint: "family",
}
}
pub async fn get_by_id(&self, id: u64) -> Result<GameFamily> {
let query = GameFamilyQueryBuilder::new(GameFamilyQueryParams::new().game_family_id(id));
let request = self.api.build_request(self.endpoint, &query.build());
let mut response = self.api.execute_request::<GameFamilies>(request).await?;
match response.game_families.len() {
0 => Err(Error::ItemNotFound),
1 => Ok(response.game_families.remove(0)),
len => Err(Error::UnexpectedResponseError(format!(
"expected 1 game family but got {len}",
))),
}
}
pub async fn get_by_ids(&self, ids: Vec<u64>) -> Result<Vec<GameFamily>> {
let query = GameFamilyQueryBuilder::new(GameFamilyQueryParams::new().game_family_ids(ids));
let request = self.api.build_request(self.endpoint, &query.build());
let response = self.api.execute_request::<GameFamilies>(request).await?;
Ok(response.game_families)
}
}
#[cfg(test)]
mod tests {
use mockito::Matcher;
use super::*;
use crate::{Game, GameFamily};
#[tokio::test]
async fn get_by_id() {
let mut server = mockito::Server::new_async().await;
let api = BoardGameGeekApi {
base_url: server.url(),
client: reqwest::Client::new(),
};
let mock = server
.mock("GET", "/family")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("type".to_owned(), "boardgamefamily".to_owned()),
Matcher::UrlEncoded("id".to_owned(), "2".to_owned()),
]))
.with_status(200)
.with_body(
std::fs::read_to_string("test_data/game_family/game_family_single.xml")
.expect("failed to load test data"),
)
.create_async()
.await;
let game_family = api.game_family().get_by_id(2).await;
mock.assert_async().await;
assert!(game_family.is_ok(), "error returned when okay expected");
let game_family = game_family.unwrap();
assert_eq!(
game_family,
GameFamily {
id: 2,
name: "Game: Carcassonne".to_owned(),
alternate_names: vec!["Carcassonne: Solo-Variante".to_owned()],
image: Some("https://cf.geekdo-images.com/c_pg0WfJKn7_P33AsDS5EA__original/img/k2t0IHkPo0nzLadfSxXhtAzyU5I=/0x0/filters:format(jpeg)/pic453826.jpg".to_owned()),
thumbnail: Some("https://cf.geekdo-images.com/c_pg0WfJKn7_P33AsDS5EA__thumb/img/8RgZmSChaxESGjIdhMeIg0C9OZk=/fit-in/200x150/filters:strip_icc()/pic453826.jpg".to_owned()),
description: "Games (expansions, promos, etc.) in the \"Carcassonne\" family of games, published by Hans im Glück.\n\n\nSee this Carcassonne Series wiki for more details.".to_owned(),
games: vec![
Game {
id: 822,
name: "Carcassonne".to_owned(),
},
Game {
id: 142_057,
name: "Carcassonne Big Box".to_owned(),
},
Game {
id: 141_008,
name: "Carcassonne Big Box 2".to_owned(),
},
],
},
);
}
#[tokio::test]
async fn get_by_ids() {
let mut server = mockito::Server::new_async().await;
let api = BoardGameGeekApi {
base_url: server.url(),
client: reqwest::Client::new(),
};
let mock = server
.mock("GET", "/family")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("type".to_owned(), "boardgamefamily".to_owned()),
Matcher::UrlEncoded("id".to_owned(), "2,3".to_owned()),
]))
.with_status(200)
.with_body(
std::fs::read_to_string("test_data/game_family/game_family_multiple.xml")
.expect("failed to load test data"),
)
.create_async()
.await;
let game_families = api.game_family().get_by_ids(vec![2, 3]).await;
mock.assert_async().await;
assert!(game_families.is_ok(), "error returned when okay expected");
let game_families = game_families.unwrap();
assert_eq!(game_families.len(), 2);
assert_eq!(
game_families[0],
GameFamily {
id: 2,
name: "Game: Carcassonne".to_owned(),
alternate_names: vec!["Carcassonne: Solo-Variante".to_owned()],
image: Some("https://cf.geekdo-images.com/c_pg0WfJKn7_P33AsDS5EA__original/img/k2t0IHkPo0nzLadfSxXhtAzyU5I=/0x0/filters:format(jpeg)/pic453826.jpg".to_owned()),
thumbnail: Some("https://cf.geekdo-images.com/c_pg0WfJKn7_P33AsDS5EA__thumb/img/8RgZmSChaxESGjIdhMeIg0C9OZk=/fit-in/200x150/filters:strip_icc()/pic453826.jpg".to_owned()),
description: "Games (expansions, promos, etc.) in the \"Carcassonne\" family of games, published by Hans im Glück.\n\n\nSee this Carcassonne Series wiki for more details.".to_owned(),
games: vec![
Game {
id: 822,
name: "Carcassonne".to_owned(),
},
Game {
id: 142_057,
name: "Carcassonne Big Box".to_owned(),
},
Game {
id: 141_008,
name: "Carcassonne Big Box 2".to_owned(),
},
],
},
);
assert_eq!(
game_families[1],
GameFamily {
id: 3,
name: "Game: Catan".to_owned(),
alternate_names: vec![],
image: Some("https://cf.geekdo-images.com/FFUKDbZw6d9mAKaL9U3ymg__original/img/rulpehNOumO24_7WzaHvl7P2aac=/0x0/filters:format(jpeg)/pic1446957.jpg".to_owned()),
thumbnail: Some("https://cf.geekdo-images.com/FFUKDbZw6d9mAKaL9U3ymg__thumb/img/o06DBHHSC9Yck1WmSkp-rK360QI=/fit-in/200x150/filters:strip_icc()/pic1446957.jpg".to_owned()),
description: "This is the family of Settlers of Catan games, meant to include any game in the Game: Catan universe.\n\nA detailed overview is given on the Catan Series wiki.".to_owned(),
games: vec![
Game {
id: 13,
name: "CATAN".to_owned(),
},
Game {
id: 27710,
name: "Catan Dice Game".to_owned(),
},
],
},
);
}
#[tokio::test]
async fn get_by_id_not_found() {
let mut server = mockito::Server::new_async().await;
let api = BoardGameGeekApi {
base_url: server.url(),
client: reqwest::Client::new(),
};
let mock = server
.mock("GET", "/family")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("type".to_owned(), "boardgamefamily".to_owned()),
Matcher::UrlEncoded("id".to_owned(), "9000".to_owned()),
]))
.with_status(200)
.with_body(
std::fs::read_to_string("test_data/game_family/game_family_not_found.xml")
.expect("failed to load test data"),
)
.create_async()
.await;
let game_families = api.game_family().get_by_id(9000).await;
mock.assert_async().await;
assert!(game_families.is_err());
assert!(matches!(game_families.err().unwrap(), Error::ItemNotFound));
}
}