use std::{
borrow::Cow,
collections::{BTreeSet, HashMap},
fmt::Display,
};
use http::Method;
use serde::{Deserialize, Serialize};
use super::{
categories::CategoryId,
endpoint::Endpoint,
error::BodyError,
games::GameId,
levels::LevelId,
platforms::PlatformId,
query_params::QueryParams,
regions::RegionId,
users::UserId,
variables::{ValueId, VariableId},
Direction, Pageable,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum RunEmbeds {
Game,
Category,
Level,
Players,
Region,
Platform,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum RunStatus {
New,
Verified,
Rejected,
}
#[derive(Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "kebab-case")]
pub enum RunsSorting {
Game,
Category,
Level,
Platform,
Region,
Emulated,
Date,
Submitted,
Status,
VerifyDate,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[serde(tag = "rel")]
pub enum Player<'a> {
User {
id: UserId<'a>,
},
Guest {
name: Cow<'a, str>,
},
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "kebab-case")]
#[serde(untagged)]
pub enum SplitsIo {
Id(String),
Url(url::Url),
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum ValueType<'a> {
PreDefined {
value: ValueId<'a>,
},
UserDefined {
value: ValueId<'a>,
},
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "kebab-case")]
#[serde(tag = "status")]
pub enum NewStatus {
Verified,
Rejected {
reason: String,
},
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
pub struct RunId<'a>(Cow<'a, str>);
impl<'a> RunId<'a> {
pub fn new<T>(id: T) -> Self
where
T: Into<Cow<'a, str>>,
{
Self(id.into())
}
}
impl<'a, T> From<T> for RunId<'a>
where
T: Into<Cow<'a, str>>,
{
fn from(value: T) -> Self {
Self::new(value)
}
}
impl Display for RunId<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
#[derive(Default, Debug, Builder, Serialize, Clone)]
#[builder(default, setter(into, strip_option))]
#[serde(rename_all = "kebab-case")]
pub struct Runs<'a> {
#[doc = r"Return only runs done by `user`."]
user: Option<UserId<'a>>,
#[doc = r"Return only runs done by `guest`."]
guest: Option<Cow<'a, str>>,
#[doc = r"Return only runs examined by `examiner`."]
examiner: Option<UserId<'a>>,
#[doc = r"Restrict results to `game`."]
game: Option<GameId<'a>>,
#[doc = r"Restrict results to `level`."]
level: Option<LevelId<'a>>,
#[doc = r"Restrict results to `category`."]
category: Option<CategoryId<'a>>,
#[doc = r"Restrict results to `platform`."]
platform: Option<PlatformId<'a>>,
#[doc = r"Restrict results to `region`."]
region: Option<RegionId<'a>>,
#[doc = r"Only return games run on an emulator when `true`."]
emulated: Option<bool>,
#[doc = r"Filter runs based on status."]
status: Option<RunStatus>,
#[doc = r"Sorting options for results."]
orderby: Option<RunsSorting>,
#[doc = r"Sort direction"]
direction: Option<Direction>,
#[builder(setter(name = "_embed"), private)]
#[serde(serialize_with = "super::utils::serialize_as_csv")]
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
embed: BTreeSet<RunEmbeds>,
}
#[derive(Debug, Builder, Serialize, Clone)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "kebab-case")]
pub struct Run<'a> {
#[doc = r"`ID` of the run."]
id: RunId<'a>,
}
#[derive(Debug, Builder, Serialize, Clone)]
#[builder(setter(into, strip_option), build_fn(validate = "Self::validate"))]
#[serde(rename_all = "kebab-case")]
pub struct CreateRun<'a> {
#[doc = r"Category ID for the run."]
category: CategoryId<'a>,
#[doc = r"Level ID for individual level runs."]
#[builder(default)]
level: Option<LevelId<'a>>,
#[doc = r"Optional date the run was performed (defaults to the current date)."]
#[builder(default)]
date: Option<Cow<'a, str>>,
#[doc = r"Optional region for the run. Some games require a region to be submitted."]
#[builder(default)]
region: Option<RegionId<'a>>,
#[doc = r"Optional platform for the run. Some games require a platform to be submitted."]
#[builder(default)]
platform: Option<PlatformId<'a>>,
#[doc = r"If the run has been verified by a moderator. Can only be set if the submitting user is a moderator of the game."]
#[builder(default)]
verified: Option<bool>,
#[builder(setter(name = "_times"), private, default)]
times: Times,
#[builder(setter(name = "_players"), private, default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
players: Vec<Player<'a>>,
#[doc = r"When `true` the run was performed on an emulator (default: false)."]
emulated: Option<bool>,
#[doc = r"A valid video URL. Optional, but some games require a video to be included."]
#[builder(default)]
video: Option<url::Url>,
#[doc = r"Optional comment on the run. Can include additional video URLs."]
#[builder(default)]
comment: Option<String>,
#[doc = r"Splits.io ID or URL for the splits for the run."]
#[builder(default)]
splitsio: Option<SplitsIo>,
#[doc = r"Variable values for the new run. Some games have mandatory variables."]
#[builder(default)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
variables: HashMap<VariableId<'a>, ValueType<'a>>,
}
#[derive(Default, Debug, Serialize, Clone)]
#[serde(rename_all = "snake_case")]
struct Times {
realtime: Option<f64>,
realtime_noloads: Option<f64>,
ingame: Option<f64>,
}
#[derive(Debug, Builder, Serialize, Clone)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "kebab-case")]
pub struct UpdateRunStatus<'a> {
#[doc = r"`ID` of the run."]
#[serde(skip)]
id: RunId<'a>,
#[doc = r"Updated status for the run."]
status: NewStatus,
}
#[derive(Debug, Builder, Serialize, Clone)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "kebab-case")]
pub struct UpdateRunPlayers<'a> {
#[doc = r"`ID` of the run."]
#[serde(skip)]
id: RunId<'a>,
#[builder(setter(name = "_players"), private)]
players: Vec<Player<'a>>,
}
#[derive(Debug, Builder, Serialize, Clone)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "kebab-case")]
pub struct DeleteRun<'a> {
#[doc = r"`ID` of the run."]
id: RunId<'a>,
}
impl Runs<'_> {
pub fn builder<'a>() -> RunsBuilder<'a> {
RunsBuilder::default()
}
}
impl RunsBuilder<'_> {
pub fn embed(&mut self, embed: RunEmbeds) -> &mut Self {
self.embed.get_or_insert_with(BTreeSet::new).insert(embed);
self
}
pub fn embeds<I>(&mut self, iter: I) -> &mut Self
where
I: Iterator<Item = RunEmbeds>,
{
self.embed.get_or_insert_with(BTreeSet::new).extend(iter);
self
}
}
impl Run<'_> {
pub fn builder<'a>() -> RunBuilder<'a> {
RunBuilder::default()
}
}
impl CreateRun<'_> {
pub fn buider<'a>() -> CreateRunBuilder<'a> {
CreateRunBuilder::default()
}
}
impl<'a> CreateRunBuilder<'a> {
pub fn realtime<T: Into<f64>>(&mut self, value: T) -> &mut Self {
self.times.get_or_insert_with(Times::default).realtime = Some(value.into());
self
}
pub fn realtime_noloads<T: Into<f64>>(&mut self, value: T) -> &mut Self {
self.times
.get_or_insert_with(Times::default)
.realtime_noloads = Some(value.into());
self
}
pub fn ingame<T: Into<f64>>(&mut self, value: T) -> &mut Self {
self.times.get_or_insert_with(Times::default).ingame = Some(value.into());
self
}
pub fn player(&mut self, player: Player<'a>) -> &mut Self {
self.players.get_or_insert_with(Vec::new).push(player);
self
}
pub fn players<I>(&mut self, iter: I) -> &mut Self
where
I: Iterator<Item = Player<'a>>,
{
self.players.get_or_insert_with(Vec::new).extend(iter);
self
}
fn validate(&self) -> Result<(), String> {
if let Some(times) = &self.times {
if times.realtime.is_none()
&& times.realtime_noloads.is_none()
&& times.ingame.is_none()
{
return Err("At least one time must be set. Set one of `realtime`, \
`realtime_noloads`, or `ingame`."
.into());
}
}
Ok(())
}
}
impl UpdateRunStatus<'_> {
pub fn builder<'a>() -> UpdateRunStatusBuilder<'a> {
UpdateRunStatusBuilder::default()
}
}
impl UpdateRunPlayers<'_> {
pub fn builder<'a>() -> UpdateRunPlayersBuilder<'a> {
UpdateRunPlayersBuilder::default()
}
}
impl<'a> UpdateRunPlayersBuilder<'a> {
pub fn player(&mut self, player: Player<'a>) -> &mut Self {
self.players.get_or_insert_with(Vec::new).push(player);
self
}
pub fn players<I>(&mut self, iter: I) -> &mut Self
where
I: Iterator<Item = Player<'a>>,
{
self.players.get_or_insert_with(Vec::new).extend(iter);
self
}
}
impl DeleteRun<'_> {
pub fn builder<'a>() -> DeleteRunBuilder<'a> {
DeleteRunBuilder::default()
}
}
impl RunEmbeds {
fn as_str(&self) -> &'static str {
match self {
RunEmbeds::Game => "game",
RunEmbeds::Category => "category",
RunEmbeds::Level => "level",
RunEmbeds::Players => "players",
RunEmbeds::Region => "region",
RunEmbeds::Platform => "platform",
}
}
}
impl Default for RunsSorting {
fn default() -> Self {
Self::Game
}
}
impl Endpoint for Runs<'_> {
fn endpoint(&self) -> Cow<'static, str> {
"/runs".into()
}
fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
QueryParams::with(self)
}
}
impl Endpoint for Run<'_> {
fn endpoint(&self) -> Cow<'static, str> {
format!("/runs/{}", self.id).into()
}
}
impl Endpoint for CreateRun<'_> {
fn method(&self) -> Method {
Method::POST
}
fn endpoint(&self) -> Cow<'static, str> {
"/runs".into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, super::error::BodyError> {
Ok(serde_json::to_vec(self).map(|body| Some(("application/json", body)))?)
}
fn requires_authentication(&self) -> bool {
true
}
}
impl Endpoint for UpdateRunStatus<'_> {
fn method(&self) -> Method {
Method::PUT
}
fn endpoint(&self) -> Cow<'static, str> {
format!("/runs/{}/status", self.id).into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, super::error::BodyError> {
Ok(serde_json::to_vec(self).map(|body| Some(("application/json", body)))?)
}
fn requires_authentication(&self) -> bool {
true
}
}
impl Endpoint for UpdateRunPlayers<'_> {
fn method(&self) -> Method {
Method::PUT
}
fn endpoint(&self) -> Cow<'static, str> {
format!("/runs/{}/players", self.id).into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, super::error::BodyError> {
Ok(serde_json::to_vec(self).map(|body| Some(("application/json", body)))?)
}
fn requires_authentication(&self) -> bool {
true
}
}
impl Endpoint for DeleteRun<'_> {
fn method(&self) -> Method {
Method::DELETE
}
fn endpoint(&self) -> Cow<'static, str> {
format!("/runs/{}", self.id).into()
}
fn requires_authentication(&self) -> bool {
true
}
}
impl From<&RunEmbeds> for &'static str {
fn from(value: &RunEmbeds) -> Self {
value.as_str()
}
}
impl Pageable for Runs<'_> {}