use crate::utils::{self, Params};
use anyhow::{anyhow, Result};
use serde_json::Value;
use std::fmt;
pub enum Thing {
BoardGame,
BoardGameExpansion,
BoardGameAccessory,
VideoGame,
RpgItem,
RpgIssue,
}
impl Thing {
pub fn as_str(&self) -> &'static str {
return match self {
Thing::BoardGame => "boardgame",
Thing::BoardGameExpansion => "boardgameexpansion",
Thing::BoardGameAccessory => "boardgameaccessory",
Thing::VideoGame => "videogame",
Thing::RpgItem => "rpgitem",
Thing::RpgIssue => "rpgissue",
};
}
pub fn to_string(&self) -> String {
return self.as_str().to_string();
}
}
impl fmt::Display for Thing {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "{}", self.as_str());
}
}
pub enum Search {
BoardGame,
BoardGameExpansion,
BoardGameAccessory,
VideoGame,
RpgItem,
}
impl Search {
pub fn as_str(&self) -> &'static str {
return match self {
Search::BoardGame => "boardgame",
Search::BoardGameExpansion => "boardgameexpansion",
Search::BoardGameAccessory => "boardgameaccessory",
Search::VideoGame => "videogame",
Search::RpgItem => "rpgitem",
};
}
pub fn to_string(&self) -> String {
return self.as_str().to_string();
}
}
impl fmt::Display for Search {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "{}", self.as_str());
}
}
pub enum Family {
Rpg,
RpgPeriodical,
BoardGameFamily,
}
impl Family {
pub fn as_str(&self) -> &'static str {
return match self {
Family::Rpg => "rpg",
Family::RpgPeriodical => "rpgperiodical",
Family::BoardGameFamily => "boardgamefamily",
};
}
pub fn to_string(&self) -> String {
return self.as_str().to_string();
}
}
impl fmt::Display for Family {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "{}", self.as_str());
}
}
pub enum ThingFamily {
Thing,
Family,
}
impl ThingFamily {
pub fn as_str(&self) -> &'static str {
return match self {
ThingFamily::Thing => "thing",
ThingFamily::Family => "family",
};
}
pub fn to_string(&self) -> String {
return self.as_str().to_string();
}
}
impl fmt::Display for ThingFamily {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "{}", self.as_str());
}
}
pub enum Hotness {
BoardGame,
Rpg,
VideoGame,
BoardGamePerson,
RpgPerson,
BoardGameCompany,
RpgCompany,
VideoGameCompany,
}
impl Hotness {
pub fn as_str(&self) -> &'static str {
return match self {
Hotness::BoardGame => "boardgame",
Hotness::Rpg => "rpg",
Hotness::VideoGame => "videogame",
Hotness::BoardGamePerson => "boardgameperson",
Hotness::RpgPerson => "rpgperson",
Hotness::BoardGameCompany => "boardgamecompany",
Hotness::RpgCompany => "rpgcompany",
Hotness::VideoGameCompany => "videogamecompany",
};
}
pub fn to_string(&self) -> String {
return self.as_str().to_string();
}
}
impl fmt::Display for Hotness {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "{}", self.as_str());
}
}
pub struct Client2 {
pub url_base: String,
pub api_prefix: String,
pub api_key: String,
}
impl Client2 {
pub fn new(api_key: &str, url_base: Option<String>, api_prefix: Option<String>) -> Self {
let ub;
let prefix;
let api_key = api_key.to_string();
if let Some(u) = url_base {
ub = match u.strip_suffix('/') {
Some(stripped) => stripped.to_string(),
None => u,
};
} else {
ub = "https://boardgamegeek.com".to_string();
}
if let Some(p) = api_prefix {
prefix = p.as_str().trim_matches('/').to_string();
} else {
prefix = "xmlapi2".to_string();
}
return Self {
url_base: ub,
api_prefix: prefix,
api_key: api_key,
};
}
pub fn new_from_defaults(api_key: &str) -> Self {
return Self::new(api_key, None, None);
}
pub async fn search(
&self,
query: &str,
stypes: &Vec<Search>,
options: Option<Params>,
) -> Result<Value> {
let params = Params::from([
("query".into(), query.into()),
(
"type".into(),
stypes
.iter()
.map(|t| t.as_str())
.collect::<Vec<&'static str>>()
.join(","),
),
]);
let url = self.get_full_url("search".into(), options, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub fn search_b(
&self,
query: &str,
stypes: &Vec<Search>,
options: Option<Params>,
) -> Result<Value> {
let params = Params::from([
("query".into(), query.into()),
(
"type".into(),
stypes
.iter()
.map(|t| t.as_str())
.collect::<Vec<&'static str>>()
.join(","),
),
]);
let url = self.get_full_url("search".into(), options, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
pub async fn thing(
&self,
ids: &Vec<usize>,
ttypes: &Vec<Thing>,
options: Option<Params>,
) -> Result<Value> {
let sids: Vec<String> = ids.iter().map(|i| i.to_string()).collect();
let params = Params::from([
("id".into(), sids.join(",")),
(
"type".into(),
ttypes
.iter()
.map(|t| t.as_str())
.collect::<Vec<&'static str>>()
.join(","),
),
]);
let url = self.get_full_url("thing".into(), options, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub fn thing_b(
&self,
ids: &Vec<usize>,
ttypes: &Vec<Thing>,
options: Option<Params>,
) -> Result<Value> {
let sids: Vec<String> = ids.iter().map(|i| i.to_string()).collect();
let params = Params::from([
("id".into(), sids.join(",")),
(
"type".into(),
ttypes
.iter()
.map(|t| t.as_str())
.collect::<Vec<&'static str>>()
.join(","),
),
]);
let url = self.get_full_url("thing".into(), options, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
pub async fn boardgame(&self, ids: &Vec<usize>, options: Option<Params>) -> Result<Value> {
return self.thing(ids, &vec![Thing::BoardGame], options).await;
}
pub fn boardgame_b(&self, ids: &Vec<usize>, options: Option<Params>) -> Result<Value> {
return self.thing_b(ids, &vec![Thing::BoardGame], options);
}
pub async fn boardgameexpansion(
&self,
ids: &Vec<usize>,
options: Option<Params>,
) -> Result<Value> {
return self
.thing(ids, &vec![Thing::BoardGameExpansion], options)
.await;
}
pub fn boardgameexpansion_b(&self, ids: &Vec<usize>, options: Option<Params>) -> Result<Value> {
return self.thing_b(ids, &vec![Thing::BoardGameExpansion], options);
}
pub async fn boardgameaccessory(
&self,
ids: &Vec<usize>,
options: Option<Params>,
) -> Result<Value> {
return self
.thing(ids, &vec![Thing::BoardGameAccessory], options)
.await;
}
pub fn boardgameaccessory_b(&self, ids: &Vec<usize>, options: Option<Params>) -> Result<Value> {
return self.thing_b(ids, &vec![Thing::BoardGameAccessory], options);
}
pub async fn videogame(&self, ids: &Vec<usize>, options: Option<Params>) -> Result<Value> {
return self.thing(ids, &vec![Thing::VideoGame], options).await;
}
pub fn videogame_b(&self, ids: &Vec<usize>, options: Option<Params>) -> Result<Value> {
return self.thing_b(ids, &vec![Thing::VideoGame], options);
}
pub async fn rpgitem(&self, ids: &Vec<usize>, options: Option<Params>) -> Result<Value> {
return self.thing(ids, &vec![Thing::RpgItem], options).await;
}
pub fn rpgitem_b(&self, ids: &Vec<usize>, options: Option<Params>) -> Result<Value> {
return self.thing_b(ids, &vec![Thing::RpgItem], options);
}
pub async fn rpgissue(&self, ids: &Vec<usize>, options: Option<Params>) -> Result<Value> {
return self.thing(ids, &vec![Thing::RpgIssue], options).await;
}
pub fn rpgissue_b(&self, ids: &Vec<usize>, options: Option<Params>) -> Result<Value> {
return self.thing_b(ids, &vec![Thing::RpgIssue], options);
}
pub async fn family(&self, ids: &Vec<usize>, ttypes: &Vec<Family>) -> Result<Value> {
let sids: Vec<String> = ids.iter().map(|i| i.to_string()).collect();
let params = Params::from([
("id".into(), sids.join(",")),
(
"type".into(),
ttypes
.iter()
.map(|t| t.as_str())
.collect::<Vec<&'static str>>()
.join(","),
),
]);
let url = self.get_full_url("family".into(), None, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub fn family_b(&self, ids: &Vec<usize>, ttypes: &Vec<Family>) -> Result<Value> {
let sids: Vec<String> = ids.iter().map(|i| i.to_string()).collect();
let params = Params::from([
("id".into(), sids.join(",")),
(
"type".into(),
ttypes
.iter()
.map(|t| t.as_str())
.collect::<Vec<&'static str>>()
.join(","),
),
]);
let url = self.get_full_url("family".into(), None, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
pub async fn rpg(&self, ids: &Vec<usize>) -> Result<Value> {
return self.family(ids, &vec![Family::Rpg]).await;
}
pub fn rpg_b(&self, ids: &Vec<usize>) -> Result<Value> {
return self.family_b(ids, &vec![Family::Rpg]);
}
pub async fn rpgperiodical(&self, ids: &Vec<usize>) -> Result<Value> {
return self.family(ids, &vec![Family::RpgPeriodical]).await;
}
pub fn rpgperiodical_b(&self, ids: &Vec<usize>) -> Result<Value> {
return self.family_b(ids, &vec![Family::RpgPeriodical]);
}
pub async fn boardgamefamily(&self, ids: &Vec<usize>) -> Result<Value> {
return self.family(ids, &vec![Family::BoardGameFamily]).await;
}
pub fn boardgamefamily_b(&self, ids: &Vec<usize>) -> Result<Value> {
return self.family_b(ids, &vec![Family::BoardGameFamily]);
}
pub async fn forumlist(&self, game_id: usize, ltype: ThingFamily) -> Result<Value> {
let params = Params::from([
("id".into(), game_id.to_string()),
("type".into(), ltype.to_string()),
]);
let url = self.get_full_url("forumlist".into(), None, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub fn forumlist_b(&self, game_id: usize, ltype: ThingFamily) -> Result<Value> {
let params = Params::from([
("id".into(), game_id.to_string()),
("type".into(), ltype.to_string()),
]);
let url = self.get_full_url("forumlist".into(), None, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
pub async fn forum(&self, forum_id: usize, options: Option<Params>) -> Result<Value> {
let params = Params::from([("id".into(), forum_id.to_string())]);
let url = self.get_full_url("forumlist".into(), options, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub async fn forum_b(&self, forum_id: usize, options: Option<Params>) -> Result<Value> {
let params = Params::from([("id".into(), forum_id.to_string())]);
let url = self.get_full_url("forumlist".into(), options, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
pub async fn thread(&self, thread_id: usize, options: Option<Params>) -> Result<Value> {
let params = Params::from([("id".into(), thread_id.to_string())]);
let url = self.get_full_url("thread".into(), options, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub fn thread_b(&self, thread_id: usize, options: Option<Params>) -> Result<Value> {
let params = Params::from([("id".into(), thread_id.to_string())]);
let url = self.get_full_url("thread".into(), options, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
pub async fn user(&self, username: &str, options: Option<Params>) -> Result<Value> {
let params = Params::from([("name".into(), username.into())]);
let url = self.get_full_url("user".into(), options, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub fn user_b(&self, username: &str, options: Option<Params>) -> Result<Value> {
let params = Params::from([("name".into(), username.into())]);
let url = self.get_full_url("user".into(), options, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
pub async fn guild(&self, guild_id: usize, options: Option<Params>) -> Result<Value> {
let params = Params::from([("id".into(), guild_id.to_string())]);
let url = self.get_full_url("guild".into(), options, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub fn guild_b(&self, guild_id: usize, options: Option<Params>) -> Result<Value> {
let params = Params::from([("id".into(), guild_id.to_string())]);
let url = self.get_full_url("guild".into(), options, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
pub async fn plays(
&self,
username: Option<&str>,
item_id: Option<usize>,
ttype: Option<ThingFamily>,
options: Option<Params>,
) -> Result<Value> {
if username.is_none() && (item_id.is_none() || ttype.is_none()) {
return Err(anyhow!(
"You must supply either a username or item_id + ttype"
));
}
if username.is_some() && item_id.is_some() {
return Err(anyhow!(
"You must supply either a username or item_id + ttype, not both"
));
}
let params;
if let Some(u) = username {
params = Params::from([("username".into(), u.into())]);
} else if let Some(id) = item_id {
params = Params::from([
("id".into(), id.to_string()),
("type".into(), ttype.unwrap().to_string()),
]);
} else {
return Err(anyhow!(
"We have a logic bug here as this should never happen"
));
}
let url = self.get_full_url("plays".into(), options, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub fn plays_b(
&self,
username: Option<&str>,
item_id: Option<usize>,
ttype: Option<ThingFamily>,
options: Option<Params>,
) -> Result<Value> {
if username.is_none() && (item_id.is_none() || ttype.is_none()) {
return Err(anyhow!(
"You must supply either a username or item_id + ttype"
));
}
if username.is_some() && item_id.is_some() {
return Err(anyhow!(
"You must supply either a username or item_id + ttype, not both"
));
}
let params;
if let Some(u) = username {
params = Params::from([("username".into(), u.into())]);
} else if let Some(id) = item_id {
params = Params::from([
("id".into(), id.to_string()),
("type".into(), ttype.unwrap().to_string()),
]);
} else {
return Err(anyhow!(
"We have a logic bug here as this should never happen"
));
}
let url = self.get_full_url("plays".into(), options, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
pub async fn collection(&self, username: &str, options: Option<Params>) -> Result<Value> {
let params = Params::from([("username".into(), username.into())]);
let url = self.get_full_url("collection".into(), options, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub fn collection_b(&self, username: &str, options: Option<Params>) -> Result<Value> {
let params = Params::from([("username".into(), username.into())]);
let url = self.get_full_url("collection".into(), options, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
pub async fn hot(&self, htype: Hotness) -> Result<Value> {
let params = Params::from([("type".into(), htype.to_string())]);
let url = self.get_full_url("hot".into(), None, Some(params));
let data = utils::get_json_resp(&url, &self.api_key).await?;
return Ok(data);
}
pub async fn hot_b(&self, htype: Hotness) -> Result<Value> {
let params = Params::from([("type".into(), htype.to_string())]);
let url = self.get_full_url("hot".into(), None, Some(params));
let data = utils::get_json_resp_b(&url, &self.api_key)?;
return Ok(data);
}
fn gen_url(&self, path: &str, options: Option<Params>) -> String {
let mut ret = String::new();
ret = ret + &self.url_base + "/" + &self.api_prefix + "/" + path + "?";
if let Some(opts) = options {
let qs = utils::params2qs(&opts);
ret.push_str(&qs);
}
return ret;
}
fn get_full_url(
&self,
path: String,
params: Option<Params>,
default_params: Option<Params>,
) -> String {
let mut opts = utils::get_opts(params);
if let Some(def_params) = default_params {
for (k, v) in &def_params {
opts.insert(k.into(), v.into());
}
}
let url = self.gen_url(&path, Some(opts));
return url;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
fn get_api_key() -> String {
return env::var("BGG_API").unwrap();
}
#[test]
fn test_client() {
let cl = Client2::new_from_defaults(&get_api_key());
assert_eq!(cl.url_base, "https://boardgamegeek.com".to_string());
assert_eq!(cl.api_prefix, "xmlapi2".to_string());
let base = "https://example.com";
let prefix = "/blah";
let cl = Client2::new(&get_api_key(), Some(base.to_string()), Some(prefix.to_string()));
assert_eq!(cl.url_base, base.to_string());
assert_eq!(cl.api_prefix, "blah");
}
#[test]
fn test_gen_url() {
let cl = Client2::new_from_defaults(&get_api_key());
let params = Params::from([
("search".to_string(), "this is a search".to_string()),
("exact".to_string(), "1".to_string()),
]);
let res = cl.gen_url("search", Some(params));
println!("{}", res);
assert!(res.starts_with("https://boardgamegeek.com/xmlapi2/search?"));
assert!(res.contains("search=this%20is%20a%20search"));
assert!(res.contains("&"));
assert!(res.contains("exact=1"));
let res = cl.gen_url("boardgame", None);
assert_eq!(
res,
"https://boardgamegeek.com/xmlapi2/boardgame?".to_string()
);
}
#[test]
fn test_get_full_url() {
let cl = Client2::new_from_defaults(&get_api_key());
let url = cl.get_full_url(
"search".to_string(),
None,
Some(Params::from([("search".into(), "this is a search".into())])),
);
assert_eq!(
url,
"https://boardgamegeek.com/xmlapi2/search?search=this%20is%20a%20search"
);
let url = cl.get_full_url(
"boardgame".into(),
Some(Params::from([("comments".into(), "1".into())])),
None,
);
assert_eq!(
url,
"https://boardgamegeek.com/xmlapi2/boardgame?comments=1".to_string()
);
}
}