1use std::{
5 borrow::Cow,
6 collections::{BTreeSet, HashMap},
7 fmt::Display,
8};
9
10use http::Method;
11use serde::{Deserialize, Serialize};
12
13use super::{
14 categories::CategoryId,
15 endpoint::Endpoint,
16 error::BodyError,
17 games::GameId,
18 levels::LevelId,
19 platforms::PlatformId,
20 query_params::QueryParams,
21 regions::RegionId,
22 users::UserId,
23 variables::{ValueId, VariableId},
24 Direction, Pageable,
25};
26
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
32pub enum RunEmbeds {
33 Game,
35 Category,
37 Level,
40 Players,
42 Region,
44 Platform,
46}
47
48#[derive(Debug, Serialize, Clone)]
50#[serde(rename_all = "kebab-case")]
51pub enum RunStatus {
52 New,
54 Verified,
56 Rejected,
58}
59
60#[derive(Debug, Serialize, Clone, Copy)]
62#[serde(rename_all = "kebab-case")]
63pub enum RunsSorting {
64 Game,
66 Category,
68 Level,
70 Platform,
72 Region,
74 Emulated,
76 Date,
78 Submitted,
80 Status,
82 VerifyDate,
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize)]
88#[serde(rename_all = "kebab-case")]
89#[serde(tag = "rel")]
90pub enum Player<'a> {
91 User {
93 id: UserId<'a>,
95 },
96 Guest {
98 name: Cow<'a, str>,
100 },
101}
102
103#[derive(Debug, Serialize, Clone)]
105#[serde(rename_all = "kebab-case")]
106#[serde(untagged)]
107pub enum SplitsIo {
108 Id(String),
110 Url(url::Url),
112}
113
114#[derive(Debug, Serialize, Clone)]
117#[serde(rename_all = "kebab-case")]
118pub enum ValueType<'a> {
119 PreDefined {
121 value: ValueId<'a>,
123 },
124 UserDefined {
126 value: ValueId<'a>,
128 },
129}
130
131#[derive(Debug, Serialize, Clone)]
133#[serde(rename_all = "kebab-case")]
134#[serde(tag = "status")]
135pub enum NewStatus {
136 Verified,
138 Rejected {
140 reason: String,
142 },
143}
144
145#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
147pub struct RunId<'a>(Cow<'a, str>);
148
149impl<'a> RunId<'a> {
150 pub fn new<T>(id: T) -> Self
152 where
153 T: Into<Cow<'a, str>>,
154 {
155 Self(id.into())
156 }
157}
158
159impl<'a, T> From<T> for RunId<'a>
160where
161 T: Into<Cow<'a, str>>,
162{
163 fn from(value: T) -> Self {
164 Self::new(value)
165 }
166}
167
168impl Display for RunId<'_> {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 write!(f, "{}", &self.0)
171 }
172}
173
174#[derive(Default, Debug, Builder, Serialize, Clone)]
176#[builder(default, setter(into, strip_option))]
177#[serde(rename_all = "kebab-case")]
178pub struct Runs<'a> {
179 #[doc = r"Return only runs done by `user`."]
180 user: Option<UserId<'a>>,
181 #[doc = r"Return only runs done by `guest`."]
182 guest: Option<Cow<'a, str>>,
183 #[doc = r"Return only runs examined by `examiner`."]
184 examiner: Option<UserId<'a>>,
185 #[doc = r"Restrict results to `game`."]
186 game: Option<GameId<'a>>,
187 #[doc = r"Restrict results to `level`."]
188 level: Option<LevelId<'a>>,
189 #[doc = r"Restrict results to `category`."]
190 category: Option<CategoryId<'a>>,
191 #[doc = r"Restrict results to `platform`."]
192 platform: Option<PlatformId<'a>>,
193 #[doc = r"Restrict results to `region`."]
194 region: Option<RegionId<'a>>,
195 #[doc = r"Only return games run on an emulator when `true`."]
196 emulated: Option<bool>,
197 #[doc = r"Filter runs based on status."]
198 status: Option<RunStatus>,
199 #[doc = r"Sorting options for results."]
200 orderby: Option<RunsSorting>,
201 #[doc = r"Sort direction"]
202 direction: Option<Direction>,
203 #[builder(setter(name = "_embed"), private)]
204 #[serde(serialize_with = "super::utils::serialize_as_csv")]
205 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
206 embed: BTreeSet<RunEmbeds>,
207}
208
209#[derive(Debug, Builder, Serialize, Clone)]
211#[builder(setter(into, strip_option))]
212#[serde(rename_all = "kebab-case")]
213pub struct Run<'a> {
214 #[doc = r"`ID` of the run."]
215 id: RunId<'a>,
216}
217
218#[derive(Debug, Builder, Serialize, Clone)]
222#[builder(setter(into, strip_option), build_fn(validate = "Self::validate"))]
223#[serde(rename_all = "kebab-case")]
224pub struct CreateRun<'a> {
225 #[doc = r"Category ID for the run."]
226 category: CategoryId<'a>,
227 #[doc = r"Level ID for individual level runs."]
228 #[builder(default)]
229 level: Option<LevelId<'a>>,
230 #[doc = r"Optional date the run was performed (defaults to the current date)."]
231 #[builder(default)]
232 date: Option<Cow<'a, str>>,
233 #[doc = r"Optional region for the run. Some games require a region to be submitted."]
234 #[builder(default)]
235 region: Option<RegionId<'a>>,
236 #[doc = r"Optional platform for the run. Some games require a platform to be submitted."]
237 #[builder(default)]
238 platform: Option<PlatformId<'a>>,
239 #[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."]
240 #[builder(default)]
241 verified: Option<bool>,
242 #[builder(setter(name = "_times"), private, default)]
243 times: Times,
244 #[builder(setter(name = "_players"), private, default)]
245 #[serde(skip_serializing_if = "Vec::is_empty")]
246 players: Vec<Player<'a>>,
247 #[doc = r"When `true` the run was performed on an emulator (default: false)."]
248 emulated: Option<bool>,
249 #[doc = r"A valid video URL. Optional, but some games require a video to be included."]
250 #[builder(default)]
251 video: Option<url::Url>,
252 #[doc = r"Optional comment on the run. Can include additional video URLs."]
253 #[builder(default)]
254 comment: Option<String>,
255 #[doc = r"Splits.io ID or URL for the splits for the run."]
256 #[builder(default)]
257 splitsio: Option<SplitsIo>,
258 #[doc = r"Variable values for the new run. Some games have mandatory variables."]
259 #[builder(default)]
260 #[serde(skip_serializing_if = "HashMap::is_empty")]
261 variables: HashMap<VariableId<'a>, ValueType<'a>>,
262}
263
264#[derive(Default, Debug, Serialize, Clone)]
265#[serde(rename_all = "snake_case")]
266struct Times {
267 realtime: Option<f64>,
268 realtime_noloads: Option<f64>,
269 ingame: Option<f64>,
270}
271
272#[derive(Debug, Builder, Serialize, Clone)]
278#[builder(setter(into, strip_option))]
279#[serde(rename_all = "kebab-case")]
280pub struct UpdateRunStatus<'a> {
281 #[doc = r"`ID` of the run."]
282 #[serde(skip)]
283 id: RunId<'a>,
284 #[doc = r"Updated status for the run."]
285 status: NewStatus,
286}
287
288#[derive(Debug, Builder, Serialize, Clone)]
299#[builder(setter(into, strip_option))]
300#[serde(rename_all = "kebab-case")]
301pub struct UpdateRunPlayers<'a> {
302 #[doc = r"`ID` of the run."]
303 #[serde(skip)]
304 id: RunId<'a>,
305 #[builder(setter(name = "_players"), private)]
306 players: Vec<Player<'a>>,
307}
308
309#[derive(Debug, Builder, Serialize, Clone)]
314#[builder(setter(into, strip_option))]
315#[serde(rename_all = "kebab-case")]
316pub struct DeleteRun<'a> {
317 #[doc = r"`ID` of the run."]
318 id: RunId<'a>,
319}
320
321impl Runs<'_> {
322 pub fn builder<'a>() -> RunsBuilder<'a> {
324 RunsBuilder::default()
325 }
326}
327
328impl RunsBuilder<'_> {
329 pub fn embed(&mut self, embed: RunEmbeds) -> &mut Self {
331 self.embed.get_or_insert_with(BTreeSet::new).insert(embed);
332 self
333 }
334
335 pub fn embeds<I>(&mut self, iter: I) -> &mut Self
337 where
338 I: Iterator<Item = RunEmbeds>,
339 {
340 self.embed.get_or_insert_with(BTreeSet::new).extend(iter);
341 self
342 }
343}
344
345impl Run<'_> {
346 pub fn builder<'a>() -> RunBuilder<'a> {
348 RunBuilder::default()
349 }
350}
351
352impl CreateRun<'_> {
353 pub fn buider<'a>() -> CreateRunBuilder<'a> {
355 CreateRunBuilder::default()
356 }
357}
358
359impl<'a> CreateRunBuilder<'a> {
360 pub fn realtime<T: Into<f64>>(&mut self, value: T) -> &mut Self {
362 self.times.get_or_insert_with(Times::default).realtime = Some(value.into());
363 self
364 }
365
366 pub fn realtime_noloads<T: Into<f64>>(&mut self, value: T) -> &mut Self {
368 self.times
369 .get_or_insert_with(Times::default)
370 .realtime_noloads = Some(value.into());
371 self
372 }
373
374 pub fn ingame<T: Into<f64>>(&mut self, value: T) -> &mut Self {
376 self.times.get_or_insert_with(Times::default).ingame = Some(value.into());
377 self
378 }
379
380 pub fn player(&mut self, player: Player<'a>) -> &mut Self {
382 self.players.get_or_insert_with(Vec::new).push(player);
383 self
384 }
385
386 pub fn players<I>(&mut self, iter: I) -> &mut Self
388 where
389 I: Iterator<Item = Player<'a>>,
390 {
391 self.players.get_or_insert_with(Vec::new).extend(iter);
392 self
393 }
394
395 fn validate(&self) -> Result<(), String> {
396 if let Some(times) = &self.times {
397 if times.realtime.is_none()
398 && times.realtime_noloads.is_none()
399 && times.ingame.is_none()
400 {
401 return Err("At least one time must be set. Set one of `realtime`, \
402 `realtime_noloads`, or `ingame`."
403 .into());
404 }
405 }
406 Ok(())
407 }
408}
409
410impl UpdateRunStatus<'_> {
411 pub fn builder<'a>() -> UpdateRunStatusBuilder<'a> {
413 UpdateRunStatusBuilder::default()
414 }
415}
416
417impl UpdateRunPlayers<'_> {
418 pub fn builder<'a>() -> UpdateRunPlayersBuilder<'a> {
420 UpdateRunPlayersBuilder::default()
421 }
422}
423
424impl<'a> UpdateRunPlayersBuilder<'a> {
425 pub fn player(&mut self, player: Player<'a>) -> &mut Self {
427 self.players.get_or_insert_with(Vec::new).push(player);
428 self
429 }
430
431 pub fn players<I>(&mut self, iter: I) -> &mut Self
433 where
434 I: Iterator<Item = Player<'a>>,
435 {
436 self.players.get_or_insert_with(Vec::new).extend(iter);
437 self
438 }
439}
440
441impl DeleteRun<'_> {
442 pub fn builder<'a>() -> DeleteRunBuilder<'a> {
444 DeleteRunBuilder::default()
445 }
446}
447
448impl RunEmbeds {
449 fn as_str(&self) -> &'static str {
450 match self {
451 RunEmbeds::Game => "game",
452 RunEmbeds::Category => "category",
453 RunEmbeds::Level => "level",
454 RunEmbeds::Players => "players",
455 RunEmbeds::Region => "region",
456 RunEmbeds::Platform => "platform",
457 }
458 }
459}
460
461impl Default for RunsSorting {
462 fn default() -> Self {
463 Self::Game
464 }
465}
466
467impl Endpoint for Runs<'_> {
468 fn endpoint(&self) -> Cow<'static, str> {
469 "/runs".into()
470 }
471
472 fn query_parameters(&self) -> Result<QueryParams<'_>, BodyError> {
473 QueryParams::with(self)
474 }
475}
476
477impl Endpoint for Run<'_> {
478 fn endpoint(&self) -> Cow<'static, str> {
479 format!("/runs/{}", self.id).into()
480 }
481}
482
483impl Endpoint for CreateRun<'_> {
484 fn method(&self) -> Method {
485 Method::POST
486 }
487
488 fn endpoint(&self) -> Cow<'static, str> {
489 "/runs".into()
490 }
491
492 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, super::error::BodyError> {
493 Ok(serde_json::to_vec(self).map(|body| Some(("application/json", body)))?)
494 }
495
496 fn requires_authentication(&self) -> bool {
497 true
498 }
499}
500
501impl Endpoint for UpdateRunStatus<'_> {
502 fn method(&self) -> Method {
503 Method::PUT
504 }
505
506 fn endpoint(&self) -> Cow<'static, str> {
507 format!("/runs/{}/status", self.id).into()
508 }
509
510 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, super::error::BodyError> {
511 Ok(serde_json::to_vec(self).map(|body| Some(("application/json", body)))?)
512 }
513
514 fn requires_authentication(&self) -> bool {
515 true
516 }
517}
518
519impl Endpoint for UpdateRunPlayers<'_> {
520 fn method(&self) -> Method {
521 Method::PUT
522 }
523
524 fn endpoint(&self) -> Cow<'static, str> {
525 format!("/runs/{}/players", self.id).into()
526 }
527
528 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, super::error::BodyError> {
529 Ok(serde_json::to_vec(self).map(|body| Some(("application/json", body)))?)
530 }
531
532 fn requires_authentication(&self) -> bool {
533 true
534 }
535}
536
537impl Endpoint for DeleteRun<'_> {
538 fn method(&self) -> Method {
539 Method::DELETE
540 }
541
542 fn endpoint(&self) -> Cow<'static, str> {
543 format!("/runs/{}", self.id).into()
544 }
545
546 fn requires_authentication(&self) -> bool {
547 true
548 }
549}
550
551impl From<&RunEmbeds> for &'static str {
552 fn from(value: &RunEmbeds) -> Self {
553 value.as_str()
554 }
555}
556
557impl Pageable for Runs<'_> {}