//! Client for API requests.
use crate::{
error::{ResponseError, Status},
model::{
latest_news::LatestNewsResponse,
league_leaderboard::{self, LeagueLeaderboardResponse},
searched_user::SearchedUserResponse,
server_activity::ServerActivityResponse,
server_stats::ServerStatsResponse,
stream::StreamResponse,
user::{UserRecordsResponse, UserResponse},
xp_leaderboard::{self, XPLeaderboardResponse},
},
};
use http::status::StatusCode;
use reqwest::{self, Error, Response};
use serde::Deserialize;
const API_URL: &str = "https://ch.tetr.io/api/";
/// Client for API requests.
///
/// # Examples
///
/// Creating a Client instance and getting some objects:
///
/// ```no_run
/// use tetr_ch::client::Client;
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
/// // For example, get information for user `RINRIN-RS`.
/// let user = client.get_user("rinrin-rs").await?;
/// # Ok(())
/// # }
/// ```
///
/// [See more examples](https://github.com/Rinrin0413/tetr-ch-rs/examples/)
#[non_exhaustive]
#[derive(Default)]
pub struct Client {
client: reqwest::Client,
}
type RspErr<T> = Result<T, ResponseError>;
impl Client {
/// Create a new [`Client`].
///
/// # Examples
///
/// Creating a Client instance:
///
/// ```
/// use tetr_ch::client;
///
/// let client = client::Client::new();
/// ```
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
/// Returns the user model.
///
/// # Examples
///
/// Getting a user object:
///
/// ```no_run
/// use tetr_ch::client::Client;
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
/// // Get information for user `RINRIN-RS`.
/// let user = client.get_user("rinrin-rs").await?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// Returns a [`ResponseError::DeserializeErr`] if there are some mismatches in the API docs,
/// or when this library is defective.
///
/// Returns a [`ResponseError::RequestErr`] redirect loop was detected or redirect limit was exhausted.
///
/// Returns a [`ResponseError::HttpErr`] if the HTTP request fails.
pub async fn get_user(self, user: &str) -> RspErr<UserResponse> {
let url = format!("{}/users/{}", API_URL, user.to_lowercase());
let res = self.client.get(url).send().await;
response(res).await
}
/// Returns the server stats model.
///
/// # Examples
///
/// Getting the server stats object:
///
/// ```no_run
/// use tetr_ch::client::Client;
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
/// // Get the server stats.
/// let user = client.get_server_stats().await?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// Returns a [`ResponseError::DeserializeErr`] if there are some mismatches in the API docs,
/// or when this library is defective.
///
/// Returns a [`ResponseError::RequestErr`] redirect loop was detected or redirect limit was exhausted.
///
/// Returns a [`ResponseError::HttpErr`] if the HTTP request fails.
pub async fn get_server_stats(self) -> RspErr<ServerStatsResponse> {
let url = format!("{}general/stats", API_URL);
let res = self.client.get(url).send().await;
response(res).await
}
/// Returns the server activity model.
///
/// # Examples
///
/// Getting the server activity object:
///
/// ```no_run
/// use tetr_ch::client::Client;
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
/// // Get the server activity.
/// let user = client.get_server_activity().await?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// Returns a [`ResponseError::DeserializeErr`] if there are some mismatches in the API docs,
/// or when this library is defective.
///
/// Returns a [`ResponseError::RequestErr`] redirect loop was detected or redirect limit was exhausted.
///
/// Returns a [`ResponseError::HttpErr`] if the HTTP request fails.
pub async fn get_server_activity(self) -> RspErr<ServerActivityResponse> {
let url = format!("{}general/activity", API_URL);
let res = self.client.get(url).send().await;
response(res).await
}
/// Returns the user records model.
///
/// # Examples
///
/// Getting the records object:
///
/// ```no_run
/// use tetr_ch::client::Client;
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
/// // Get the user records.
/// let user = client.get_user_records("621db46d1d638ea850be2aa0").await?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// Returns a [`ResponseError::DeserializeErr`] if there are some mismatches in the API docs,
/// or when this library is defective.
///
/// Returns a [`ResponseError::RequestErr`] redirect loop was detected or redirect limit was exhausted.
///
/// Returns a [`ResponseError::HttpErr`] if the HTTP request fails.
pub async fn get_user_records(self, user: &str) -> RspErr<UserRecordsResponse> {
let url = format!("{}/users/{}/records", API_URL, user.to_lowercase());
let res = self.client.get(url).send().await;
response(res).await
}
/// Returns the TETRA LEAGUE leaderboard model.
///
/// # Arguments
///
/// - `query`:
///
/// The query parameters.
/// This argument requires a [`query::LeagueLeaderboardQuery`].
///
/// # Examples
///
/// Getting the TETRA LEAGUE leaderboard object:
///
/// ```no_run
/// use tetr_ch::client::{Client, query::LeagueLeaderboardQuery};
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
///
/// // Set the query parameters.
/// let query = LeagueLeaderboardQuery::new()
/// // 15200TR or less.
/// .after(15200.)
/// // 3 users.
/// .limit(3)
/// // Japan.
/// .country("jp");
///
/// // Get the TETRA LEAGUE leaderboard.
/// let user = client.get_league_leaderboard(query).await?;
/// # Ok(())
/// # }
/// ```
///
/// See [here](query::LeagueLeaderboardQuery) for details on setting query parameters.
///
/// # Errors
///
/// Returns a [`ResponseError::DeserializeErr`] if there are some mismatches in the API docs,
/// or when this library is defective.
///
/// Returns a [`ResponseError::RequestErr`] redirect loop was detected or redirect limit was exhausted.
///
/// Returns a [`ResponseError::HttpErr`] if the HTTP request fails.
///
/// # Panics
///
/// Panics if the query parameter`limit` is not between 0 and 100.
///
/// ```should_panic,no_run
/// use tetr_ch::client::{
/// Client,
/// query::{LeagueLeaderboardQuery, Limit}
/// };
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
///
/// let query = LeagueLeaderboardQuery {
/// // 101 users (not allowed).
/// limit: Some(Limit::Limit(101)),
/// ..LeagueLeaderboardQuery::new()
/// };
///
/// let user = client.get_league_leaderboard(query).await?;
/// # Ok(())
/// # }
///
/// # tokio_test::block_on(run());
/// ```
pub async fn get_league_leaderboard(
self,
query: query::LeagueLeaderboardQuery,
) -> RspErr<LeagueLeaderboardResponse> {
if query.is_invalid_limit_range() {
panic!(
"The query parameter`limit` must be between 0 and 100.\n\
Received: {}",
query.limit.unwrap().to_string()
);
}
// Cloned the `query` here because the query parameters will be referenced later.
let (q, url) = if query.will_full_export() {
(
query.clone().build_as_full_export(),
format!("{}/users/lists/league/all", API_URL),
)
} else {
(
query.clone().build(),
format!("{}/users/lists/league", API_URL),
)
};
let r = self.client.get(url);
let res = match q.len() {
1 => r.query(&[&q[0]]),
2 => r.query(&[&q[0], &q[1]]),
3 => r.query(&[&q[0], &q[1], &q[2]]),
_ => r,
}
.send()
.await;
match response::<LeagueLeaderboardResponse>(res).await {
Ok(mut m) => {
let (before, after) = if let Some(b_a) = query.before_or_after {
match b_a {
query::BeforeAfter::Before(b) => (Some(b.to_string()), None),
query::BeforeAfter::After(b) => (None, Some(b.to_string())),
}
} else {
(None, None)
};
let limit = query.limit.map(|l| l.to_string());
let country = query.country;
m.query = Some(league_leaderboard::QueryCache {
before,
after,
limit,
country,
});
Ok(m)
}
Err(e) => Err(e),
}
}
/// Returns the XP leaderboard model.
///
/// # Arguments
///
/// - `query`:
///
/// The query parameters.
/// This argument requires a [`query::XPLeaderboardQuery`].
///
/// # Examples
///
/// Getting the XP leaderboard object:
///
/// ```no_run
/// use tetr_ch::client::{Client, query::XPLeaderboardQuery};
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
///
/// // Set the query parameters.
/// let query = XPLeaderboardQuery::new()
/// // 50,000,000,000,000xp or less.
/// .after(50_000_000_000_000.)
/// // 10 users.
/// .limit(10)
/// // Serbia.
/// .country("rs");
///
/// // Get the XP leaderboard.
/// let user = client.get_xp_leaderboard(query).await?;
/// # Ok(())
/// # }
/// ```
///
/// See [here](query::XPLeaderboardQuery) for details on setting query parameters.
///
///
/// # Errors
///
/// Returns a [`ResponseError::DeserializeErr`] if there are some mismatches in the API docs,
/// or when this library is defective.
///
/// Returns a [`ResponseError::RequestErr`] redirect loop was detected or redirect limit was exhausted.
///
/// Returns a [`ResponseError::HttpErr`] if the HTTP request fails.
///
/// # Panics
///
/// Panics if the query parameter`limit` is not between 1 and 100.
///
/// ```should_panic,no_run
/// use tetr_ch::client::{Client, query::XPLeaderboardQuery};
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
///
/// let query = XPLeaderboardQuery {
/// // 101 users(not allowed).
/// limit: Some(std::num::NonZeroU8::new(101).unwrap()),
/// ..XPLeaderboardQuery::new()
/// };
///
/// let user = client.get_xp_leaderboard(query).await?;
/// # Ok(())
/// # }
///
/// # tokio_test::block_on(run());
/// ```
pub async fn get_xp_leaderboard(
self,
query: query::XPLeaderboardQuery,
) -> RspErr<XPLeaderboardResponse> {
if query.is_invalid_limit_range() {
panic!(
"The query parameter`limit` must be between 1 and 100.\n\
Received: {}",
query.limit.unwrap()
);
}
// Cloned the `query` here because the query parameters will be referenced later.
let q = query.clone().build();
let url = format!("{}/users/lists/xp", API_URL);
let r = self.client.get(url);
let res = match q.len() {
1 => r.query(&[&q[0]]),
2 => r.query(&[&q[0], &q[1]]),
3 => r.query(&[&q[0], &q[1], &q[2]]),
_ => r,
}
.send()
.await;
match response::<XPLeaderboardResponse>(res).await {
Ok(mut m) => {
let (before, after) = if let Some(b_a) = query.before_or_after {
match b_a {
query::BeforeAfter::Before(b) => (Some(b.to_string()), None),
query::BeforeAfter::After(b) => (None, Some(b.to_string())),
}
} else {
(None, None)
};
let limit = query.limit.map(|l| l.to_string());
let country = query.country;
m.query = Some(xp_leaderboard::QueryCache {
before,
after,
limit,
country,
});
Ok(m)
}
Err(e) => Err(e),
}
}
/// Returns the stream model.
///
/// # Arguments
///
/// - `stream_type`:
///
/// The type of Stream.
/// Currently [`StreamType::FortyLines`], [`StreamType::Blitz`], or [`StreamType::Any`].
///
/// [`StreamType::FortyLines`]: stream::StreamType::FortyLines
/// [`StreamType::Blitz`]: stream::StreamType::Blitz
/// [`StreamType::Any`]: stream::StreamType::Any
///
/// - `stream_context`:
///
/// The context of the Stream.
/// Currently [`StreamContext::Global`], [`StreamContext::UserBest`], or [`StreamContext::UserRecent`].
///
/// [`StreamContext::Global`]: stream::StreamContext::Global
/// [`StreamContext::UserBest`]: stream::StreamContext::UserBest
/// [`StreamContext::UserRecent`]: stream::StreamContext::UserRecent
///
/// - `stream_identifier` (Optional):
///
/// If applicable.
/// For example, in the case of "userbest" or "userrecent", the user ID.
///
/// # Examples
///
/// Getting the stream object:
///
/// ```no_run
/// use tetr_ch::client::{
/// Client,
/// stream::{StreamType, StreamContext}
/// };
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
///
/// // Get the stream.
/// let user = client.get_stream(
/// // 40 LINES.
/// StreamType::FortyLines,
/// // User's best.
/// StreamContext::UserBest,
/// // User ID.
/// Some("621db46d1d638ea850be2aa0"),
/// ).await?;
/// # Ok(())
/// # }
/// ```
///
/// Go to [`stream::StreamType`] | [`stream::StreamContext`].
///
/// # Errors
///
/// Returns a [`ResponseError::DeserializeErr`] if there are some mismatches in the API docs,
/// or when this library is defective.
///
/// Returns a [`ResponseError::RequestErr`] redirect loop was detected or redirect limit was exhausted.
///
/// Returns a [`ResponseError::HttpErr`] if the HTTP request fails.
pub async fn get_stream(
self,
stream_type: stream::StreamType,
stream_context: stream::StreamContext,
stream_identifier: Option<&str>,
) -> RspErr<StreamResponse> {
let stream_id = format!(
"{}_{}{}",
stream_type.as_str(),
stream_context.as_str(),
if let Some(i) = stream_identifier {
format!("_{}", i)
} else {
String::new()
}
);
let url = format!("{}/streams/{}", API_URL, stream_id.to_lowercase());
let res = self.client.get(url).send().await;
response(res).await
}
/// Returns the latest news model.
///
/// # Arguments
///
/// - `subject`:
///
/// The news subject.
/// This argument requires a [`stream::NewsSubject`].
///
/// - `limit`:
///
/// The amount of entries to return.
/// Between 1 and 100.
/// 25 by default.
///
/// # Examples
///
/// Getting the latest news object:
///
/// ```no_run
/// use tetr_ch::client::{Client, stream::NewsSubject};
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
///
/// // Get the latest news.
/// let user = client.get_latest_news(
/// // News of the user `621db46d1d638ea850be2aa0`.
/// NewsSubject::User("621db46d1d638ea850be2aa0".to_string()),
/// // three news.
/// 3,
/// ).await?;
/// # Ok(())
/// # }
/// ```
///
/// Go to [`stream::StreamType`] | [`stream::StreamContext`].
///
/// # Errors
///
/// Returns a [`ResponseError::DeserializeErr`] if there are some mismatches in the API docs,
/// or when this library is defective.
///
/// Returns a [`ResponseError::RequestErr`] redirect loop was detected or redirect limit was exhausted.
///
/// Returns a [`ResponseError::HttpErr`] if the HTTP request fails.
///
/// # Panics
///
/// Panics if the query parameter`limit` is not between 1 and 100.
///
/// ```should_panic,no_run
/// use tetr_ch::client::{Client, stream::NewsSubject};
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
///
/// let user = client.get_latest_news(
/// NewsSubject::User("621db46d1d638ea850be2aa0".to_string()),
/// // 101 news.
/// 101,
/// ).await?;
/// # Ok(())
/// # }
///
/// # tokio_test::block_on(run());
/// ```
pub async fn get_latest_news(
self,
subject: stream::NewsSubject,
limit: u8,
) -> RspErr<LatestNewsResponse> {
if !(1..=100).contains(&limit) {
// !(1 <= limit && limit <= 100)
panic!(
"The query parameter`limit` must be between 1 and 100.\n\
Received: {}",
limit
);
}
use stream::NewsSubject;
let url = format!(
"{}/news/{}",
API_URL,
match subject {
NewsSubject::Any => String::new(),
NewsSubject::Global => "global".to_string(),
NewsSubject::User(id) => format!("user_{}", id),
}
);
let res = self.client.get(url).query(&[("limit", limit)]).send().await;
response(res).await
}
/// Search a TETR.IO user account by Discord account.
///
/// # Arguments
///
/// - `discord_user`:
///
/// The Discord username or Discord ID to look up.
///
/// # Examples
///
/// Search a user by Discord account:
///
/// ```no_run
/// use tetr_ch::client::Client;
/// # use std::io;
///
/// # async fn run() -> io::Result<()> {
/// let client = Client::new();
///
/// // Search a user by Discord ID.
/// let user = client.search_user("724976600873041940").await?;
/// # Ok(())
/// # }
///
/// # tokio_test::block_on(run());
/// ```
///
/// # Errors
///
/// Returns a [`ResponseError::DeserializeErr`] if there are some mismatches in the API docs,
/// or when this library is defective.
///
/// Returns a [`ResponseError::RequestErr`] redirect loop was detected or redirect limit was exhausted.
///
/// Returns a [`ResponseError::HttpErr`] if the HTTP request fails.
pub async fn search_user(self, discord_user: &str) -> RspErr<SearchedUserResponse> {
let url = format!("{}/users/search/{}", API_URL, discord_user);
let res = self.client.get(url).send().await;
response(res).await
}
}
/// Receives `Result<Response, Error>` and returns `Result<T, ResponseError>`.
///
/// # Examples
///
/// ```ignore
/// let res = self.client.get(url).send().await;
/// response(res).await
/// ```
async fn response<T>(response: Result<Response, Error>) -> RspErr<T>
where
for<'de> T: Deserialize<'de>,
{
match response {
Ok(r) => {
if !r.status().is_success() {
match StatusCode::from_u16(r.status().as_u16()) {
Ok(c) => return Err(ResponseError::HttpErr(Status::Valid(c))),
Err(e) => return Err(ResponseError::HttpErr(Status::Invalid(e))),
}
}
match r.json().await {
Ok(m) => Ok(m),
Err(e) => Err(ResponseError::DeserializeErr(e.to_string())),
}
}
Err(e) => Err(ResponseError::RequestErr(e.to_string())),
}
}
pub mod query {
//! Structs for query parameters.
use std::num::NonZeroU8;
/// A struct for query parameters for the TETRA LEAGUE leaderboard.
///
/// `None` means default value.
///
/// This structure manages the following four query parameters:
///
/// - `before`(f64): The lower bound in TR.
/// Use this to paginate upwards.
/// Take the highest seen TR and pass that back through this field to continue scrolling.
/// If set, the search order is reversed (returning the lowest items that match the query)
/// This parameter is ignored if specified to get the full leaderboard.
///
/// - `after`(f64): The upper bound in TR.
/// Use this to paginate downwards.
/// Take the lowest seen TR and pass that back through this field to continue scrolling.
/// This parameter is ignored if specified to get the full leaderboard.
///
/// - `limit`(u8): The amount of entries to return, Between `0` and `100`.
/// 50 by default.
/// You can specify to get the full leaderboard by passing `0`.
/// In this case the `before` and `after` parameters are ignored.
///
/// - `country`(String): The ISO 3166-1 country code to filter to.
/// Leave unset to not filter by country.
///
/// ***The `before` and `after` parameters may not be combined.**
///
/// # Examples
///
/// ```
/// use tetr_ch::client::query::LeagueLeaderboardQuery;
///
/// // Default(25000TR or less, 50 entries) query.
/// let q1 = LeagueLeaderboardQuery::new();
///
/// // 15200TR or less, three entries, filter by Japan.
/// let q2 = LeagueLeaderboardQuery::new()
/// .after(15200.)
/// .limit(3)
/// .country("jp");
///
/// // 15200TR or higher.
/// // Also sort by TR ascending.
/// let q3 = LeagueLeaderboardQuery::new()
/// .before(15200.);
///
/// // Full leaderboard.
/// let q4 = LeagueLeaderboardQuery::new()
/// .limit(0);
///
/// // You can restore the query parameters to default as follows:
/// let mut q5 = LeagueLeaderboardQuery::new().country("us");
/// q5.init();
/// ```
#[derive(Clone, Debug, Default)]
pub struct LeagueLeaderboardQuery {
/// The bound in TR.
///
/// The `before` and `after` parameters may not be combined,
/// so either set the parameter with an enum or set it to default(after) by passing `None`.
pub before_or_after: Option<BeforeAfter>,
/// The amount of entries to return.
pub limit: Option<Limit>,
/// The ISO 3166-1 country code to filter to. Leave unset to not filter by country.
/// But some vanity flags exist.
pub country: Option<String>,
}
impl LeagueLeaderboardQuery {
/// Creates a new[`LeagueLeaderboardQuery`].
/// Values are set to default.
///
/// # Examples
///
/// Creates a new[`LeagueLeaderboardQuery`] with default parameters.
///
/// ```
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let query = LeagueLeaderboardQuery::new();
/// ```
pub fn new() -> Self {
Self::default()
}
/// Initializes the [`LeagueLeaderboardQuery`].
///
/// # Examples
///
/// Initializes the [`LeagueLeaderboardQuery`] with default parameters.
///
/// ```
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let mut query = LeagueLeaderboardQuery::new().country("us");
/// query.init();
/// ```
pub fn init(self) -> Self {
Self::default()
}
/// Set the query parameter`before`.
///
/// Disabled by default.
///
/// The `before` and `after` parameters may not be combined,
/// so even if there is an `after` parameter, the `before` parameter takes precedence and overrides it.
/// Disabled by default.
///
/// This parameter is ignored if specified to get the full leaderboard.
///
/// # Examples
///
/// Sets the query parameter`before` to `15200`.
///
/// ```
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let query = LeagueLeaderboardQuery::new().before(15200.);
/// ```
pub fn before(self, bound: f64) -> Self {
Self {
before_or_after: Some(BeforeAfter::Before(bound)),
..self
}
}
/// Set the query parameter`after`.
///
/// 25000 by default.
///
/// The `before` and `after` parameters may not be combined,
/// so even if there is a `before` parameter, the `after` parameter takes precedence and overrides it.
///
/// This parameter is ignored if specified to get the full leaderboard.
///
/// # Examples
///
/// Sets the query parameter`after` to `15200`.
///
/// ```
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let query = LeagueLeaderboardQuery::new().after(15200.);
/// ```
pub fn after(self, bound: f64) -> Self {
Self {
before_or_after: Some(BeforeAfter::After(bound)),
..self
}
}
/// Set the query parameter`limit`
/// The amount of entries to return, Between `0` and `100`.
/// 50 by default.
///
/// You can specify to get the full leaderboard by passing `0`.
/// In this case the `before` and `after` parameters are ignored.
///
/// # Examples
///
/// Sets the query parameter`limit` to `3`.
///
/// ```
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let query = LeagueLeaderboardQuery::new().limit(3);
/// ```
///
/// # Panics
///
/// Panics if argument`limit` is not between `0` and `100`.
///
/// ```should_panic
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let query = LeagueLeaderboardQuery::new().limit(101);
/// ```
pub fn limit(self, limit: u8) -> Self {
if
/*0 <= limit && */
limit <= 100 {
Self {
limit: Some(if limit == 0 {
Limit::Full
} else {
Limit::Limit(limit)
}),
..self
}
} else {
panic!(
"The argument`limit` must be between and 100.\n\
Received: {}",
limit
);
}
}
/// Set the query parameter`country`.
///
/// # Examples
///
/// Sets the query parameter`country` to `jp`.
///
/// ```
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let query = LeagueLeaderboardQuery::new().country("jp");
/// ```
pub fn country(self, country: &str) -> Self {
Self {
country: Some(country.to_owned().to_uppercase()),
..self
}
}
/// Whether the query parameters`limit` is out of bounds.
///
/// # Examples
///
/// ```
/// # use tetr_ch::client::query::{LeagueLeaderboardQuery, Limit};
/// let invalid_query = LeagueLeaderboardQuery{
/// limit: Some(Limit::Limit(101)),
/// ..LeagueLeaderboardQuery::new()
/// };
/// assert!(invalid_query.is_invalid_limit_range());
/// ```
#[allow(clippy::nonminimal_bool)]
pub fn is_invalid_limit_range(&self) -> bool {
if let Some(l) = self.limit.clone() {
match l {
Limit::Limit(l) => !(l <= 100),
Limit::Full => false,
}
} else {
false
}
}
/// Whether the query parameters`limit` specifies to get the full leaderboard.
///
/// # Examples
///
/// ```
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let query = LeagueLeaderboardQuery::new().limit(0);
/// assert!(query.will_full_export());
/// ```
pub fn will_full_export(&self) -> bool {
if let Some(l) = self.limit.clone() {
match l {
Limit::Limit(l) => l == 0,
Limit::Full => true,
}
} else {
false
}
}
/// Builds the query parameters to `Vec<(String, String)>`.
///
/// # Examples
///
/// ```ignore
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let query = LeagueLeaderboardQuery::new();
/// let query_params = query.build();
/// ```
pub(crate) fn build(mut self) -> Vec<(String, String)> {
// For not pass "Full" to puery parameters.
if self.will_full_export() {
self.limit = Some(Limit::Full);
}
let mut result = Vec::new();
if let Some(b_a) = self.before_or_after.clone() {
match b_a {
BeforeAfter::Before(b) => result.push(("before".to_string(), b.to_string())),
BeforeAfter::After(b) => result.push(("after".to_string(), b.to_string())),
}
}
if let Some(l) = self.limit.clone() {
if !self.will_full_export() {
result.push(("limit".to_string(), l.to_string()));
}
}
if let Some(c) = self.country {
result.push(("country".to_string(), c));
}
result
}
/// Builds the query parameters to `Vec<(String, String)>` as full export.
///
/// # Examples
///
/// ```ignore
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let query = LeagueLeaderboardQuery::new().limit(0);
/// let query_params = query.build_full_export();
/// ```
pub(crate) fn build_as_full_export(mut self) -> Vec<(String, String)> {
// For not pass "Full" to puery parameters.
if self.will_full_export() {
self.limit = Some(Limit::Full);
}
let mut result = Vec::new();
result.push(("limit".to_string(), "0".to_string()));
if let Some(c) = self.country {
result.push(("country".to_string(), c));
}
result
}
/// Initializes the [`LeagueLeaderboardQuery`].
///
/// # Examples
///
/// ```ignore
/// # use tetr_ch::client::query::LeagueLeaderboardQuery;
/// let default_query = LeagueLeaderboardQuery::default();
/// ```
fn default() -> Self {
Self {
before_or_after: None,
limit: None,
country: None,
}
}
}
/// Amount of entries to return.
#[derive(Clone, Debug)]
pub enum Limit {
/// Between 1 and 100. 50 by default.
Limit(u8),
Full,
}
impl ToString for Limit {
fn to_string(&self) -> String {
match self {
Limit::Limit(l) => {
if l == &0 {
"Full".to_string()
} else {
l.to_string()
}
}
Limit::Full => "Full".to_string(),
}
}
}
/// A struct for query parameters for the XP leaderboard.
///
/// `None` means default value.
///
/// This structure manages the following four query parameters:
///
/// - `before`(f64): The lower bound in XP.
/// Use this to paginate upwards.
/// Take the highest seen XP and pass that back through this field to continue scrolling.
/// If set, the search order is reversed (returning the lowest items that match the query)
///
/// - `after`(f64): The upper bound in XP.
/// Use this to paginate downwards.
/// Take the lowest seen XP and pass that back through this field to continue scrolling.
/// Infinite([`f64::INFINITY`]) by default.
///
/// - `limit`([NonZeroU8]): The amount of entries to return.
/// Between 1 and 100.
/// 50 by default.
///
/// - `country`(String): The ISO 3166-1 country code to filter to.
/// Leave unset to not filter by country.
///
/// ***The `before` and `after` parameters may not be combined.**
///
/// # Examples
///
/// ```
/// use tetr_ch::client::query::XPLeaderboardQuery;
///
/// // Default(descending, fifty entries) query.
/// let q1 = XPLeaderboardQuery::new();
///
/// // 50,000,000,000,000xp or less, thirty entries, filter by Japan.
/// let q2 = XPLeaderboardQuery::new()
/// .after(50_000_000_000_000.)
/// .limit(3)
/// .country("jp");
///
/// // 50,000,000,000,000xp or higher.
/// // Also sort by XP ascending.
/// let q3 = XPLeaderboardQuery::new()
/// .before(50_000_000_000_000.);
///
/// // You can restore the query parameters to default as follows:
/// let mut q4 = XPLeaderboardQuery::new().country("us");
/// q4.init();
/// ```
#[derive(Clone, Debug, Default)]
pub struct XPLeaderboardQuery {
/// The bound in XP.
///
/// The `before` and `after` parameters may not be combined,
/// so either set the parameter with an enum or set it to default(after) by passing `None`.
pub before_or_after: Option<BeforeAfter>,
/// The amount of entries to return.
/// Between 1 and 100. 50 by default.
pub limit: Option<NonZeroU8>,
/// The ISO 3166-1 country code to filter to. Leave unset to not filter by country.
/// But some vanity flags exist.
pub country: Option<String>,
}
impl XPLeaderboardQuery {
/// Creates a new[`XPLeaderboardQuery`].
/// Values are set to default.
///
/// # Examples
///
/// Creates a new[`XPLeaderboardQuery`] with default parameters.
///
/// ```
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// let query = XPLeaderboardQuery::new();
/// ```
pub fn new() -> Self {
Self::default()
}
/// Initializes the [`XPLeaderboardQuery`].
///
/// # Examples
///
/// Initializes the [`XPLeaderboardQuery`] with default parameters.
///
/// ```
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// let mut query = XPLeaderboardQuery::new();
/// query.init();
/// ```
pub fn init(self) -> Self {
Self::default()
}
/// Set the query parameter`before`.
///
/// The `before` and `after` parameters may not be combined,
/// so even if there is an `after` parameter, the `before` parameter takes precedence and overrides it.
/// Disabled by default.
///
/// # Examples
///
/// Sets the query parameter`before` to `50,000,000,000,000`.
///
/// ```
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// let mut query = XPLeaderboardQuery::new()
/// .before(50_000_000_000_000.);
/// ```
pub fn before(self, bound: f64) -> Self {
Self {
before_or_after: if bound.is_infinite() {
Some(BeforeAfter::Before(bound))
} else {
None
},
..self
}
}
/// Set the query parameter`after`.
///
/// The `before` and `after` parameters may not be combined,
/// so even if there is a `before` parameter, the `after` parameter takes precedence and overrides it.
/// Infinite([`f64::INFINITY`]) by default.
///
/// # Examples
///
/// Sets the query parameter`after` to `50,000,000,000,000`.
///
/// ```
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// let mut query = XPLeaderboardQuery::new()
/// .after(50_000_000_000_000.);
/// ```
pub fn after(self, bound: f64) -> Self {
Self {
before_or_after: Some(BeforeAfter::After(bound)),
..self
}
}
/// Set the query parameter`limit`
/// The amount of entries to return, Between `1` and `100`.
/// 50 by default.
///
/// # Examples
///
/// Sets the query parameter`limit` to `5`.
///
/// ```
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// let mut query = XPLeaderboardQuery::new().limit(5);
/// ```
///
/// # Panics
///
/// Panics if argument`limit` is not between `1` and `100`.
///
/// ```should_panic
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// let mut query = XPLeaderboardQuery::new().limit(0);
/// ```
///
/// ```should_panic
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// let mut query = XPLeaderboardQuery::new().limit(101);
/// ```
pub fn limit(self, limit: u8) -> Self {
if (1..=100).contains(&limit) {
// 1 <= limit && limit <= 100
Self {
limit: Some(NonZeroU8::new(limit).unwrap()),
..self
}
} else {
panic!(
"The argument`limit` must be between 1 and 100.\n\
Received: {}",
limit
);
}
}
/// Set the query parameter`country`.
///
/// # Examples
///
/// Sets the query parameter`country` to `ca`.
///
/// ```
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// let mut query = XPLeaderboardQuery::new().country("ca");
/// ```
pub fn country(self, country: &str) -> Self {
Self {
country: Some(country.to_owned().to_uppercase()),
..self
}
}
/// Whether the query parameters`limit` is out of bounds.
///
/// # Examples
///
/// ```
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// use std::num::NonZeroU8;
///
/// let invalid_query = XPLeaderboardQuery{
/// limit: Some(NonZeroU8::new(101).unwrap()),
/// ..XPLeaderboardQuery::new()
/// };
/// assert!(invalid_query.is_invalid_limit_range());
/// ```
#[allow(clippy::nonminimal_bool)]
pub fn is_invalid_limit_range(&self) -> bool {
if let Some(l) = self.limit {
!(l <= NonZeroU8::new(100).unwrap())
} else {
false
}
}
/// Builds the query parameters to `Vec<(String, String)>`.
///
/// # Examples
///
/// ```ignore
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// let query = XPLeaderboardQuery::new();
/// let query_params = query.build();
/// ```
pub(crate) fn build(mut self) -> Vec<(String, String)> {
// For not pass "inf" to puery parameters.
if let Some(BeforeAfter::After(b)) = self.before_or_after {
if b.is_infinite() {
self.before_or_after = None;
}
}
let mut result = Vec::new();
if let Some(b_a) = self.before_or_after.clone() {
match b_a {
BeforeAfter::Before(b) => result.push(("before".to_string(), b.to_string())),
BeforeAfter::After(b) => result.push(("after".to_string(), b.to_string())),
}
}
if let Some(l) = self.limit {
result.push(("limit".to_string(), l.to_string()));
}
if let Some(c) = self.country {
result.push(("country".to_string(), c));
}
result
}
/// Returns the default [`XPLeaderboardQuery`].
///
/// # Examples
///
/// ```ignore
/// # use tetr_ch::client::query::XPLeaderboardQuery;
/// let query = XPLeaderboardQuery::default();
/// ```
fn default() -> Self {
Self {
before_or_after: None,
limit: None,
country: None,
}
}
}
/// The bound.
///
/// The `before` and `after` parameters may not be combined,
/// so need to either set the parameter.
#[derive(Clone, Debug)]
pub enum BeforeAfter {
/// The lower bound.
/// Use this to paginate upwards.
/// Take the highest seen value and pass that back through this field to continue scrolling.
/// If set, the search order is reversed (returning the lowest items that match the query)
Before(f64),
/// Use this to paginate downwards.
/// Take the lowest seen value and pass that back through this field to continue scrolling.
After(f64),
}
}
pub mod stream {
//! Features for streams.
/// Enum for the stream type.
pub enum StreamType {
/// 40 LINES
FortyLines,
/// BLITZ
Blitz,
/// Any
Any,
}
impl StreamType {
/// Converts to a `&str`.
///
/// # Examples
///
/// ```ignore
/// # use tetr_ch::client::stream::StreamType;
/// let forty_lines = StreamType::FortyLines;
/// let blitz = StreamType::Blitz;
/// let any = StreamType::Any;
/// assert_eq!(forty_lines.as_str(), "40l");
/// assert_eq!(blitz.as_str(), "blitz");
/// assert_eq!(any.as_str(), "any");
/// ```
pub(crate) fn as_str(&self) -> &str {
match self {
StreamType::FortyLines => "40l",
StreamType::Blitz => "blitz",
StreamType::Any => "any",
}
}
}
/// Enum for the stream context.
pub enum StreamContext {
Global,
UserBest,
UserRecent,
}
impl StreamContext {
/// Converts to a `&str`.
///
/// # Examples
///
/// ```ignore
/// # use tetr_ch::client::stream::StreamContext;
/// let global = StreamContext::Global;
/// let user_best = StreamContext::UserBest;
/// let user_recent = StreamContext::UserRecent;
/// assert_eq!(global.as_str(), "global");
/// assert_eq!(user_best.as_str(), "user_best");
/// assert_eq!(user_recent.as_str(), "user_recent");
pub(crate) fn as_str(&self) -> &str {
match self {
StreamContext::Global => "global",
StreamContext::UserBest => "userbest",
StreamContext::UserRecent => "userrecent",
}
}
}
/// The news subject.
pub enum NewsSubject {
/// News of all users
Any,
/// Global news.
Global,
/// The news of the user.
/// Enter the user's **ID** to `String`.
User(String),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_a_new_client() {
let _ = Client::new();
}
#[test]
fn init_league_query() {
let mut _query = query::LeagueLeaderboardQuery::new();
_query.init();
}
#[test]
#[should_panic]
fn panic_invalid_limit_range_in_league_query() {
let mut _query = query::LeagueLeaderboardQuery::new();
_query.limit(101);
}
#[test]
fn init_xp_query() {
let mut _query = query::XPLeaderboardQuery::new();
_query.init();
}
#[test]
#[should_panic]
fn panic_invalid_limit_range_in_xp_query_with101() {
let mut _query = query::XPLeaderboardQuery::new();
_query.limit(101);
}
#[test]
#[should_panic]
fn panic_invalid_limit_range_in_xp_query_with0() {
let mut _query = query::XPLeaderboardQuery::new();
_query.limit(101);
}
#[test]
fn fortylines_as_str() {
assert_eq!(stream::StreamType::FortyLines.as_str(), "40l");
}
#[test]
fn blitz_as_str() {
assert_eq!(stream::StreamType::Blitz.as_str(), "blitz");
}
#[test]
fn any_as_str() {
assert_eq!(stream::StreamType::Any.as_str(), "any");
}
#[test]
fn global_as_str() {
assert_eq!(stream::StreamContext::Global.as_str(), "global");
}
#[test]
fn userbest_as_str() {
assert_eq!(stream::StreamContext::UserBest.as_str(), "userbest");
}
#[test]
fn userrecent_as_str() {
assert_eq!(stream::StreamContext::UserRecent.as_str(), "userrecent");
}
}