Skip to main content

lichess_api/model/
mod.rs

1pub mod account;
2pub mod analysis;
3pub mod arena_tournaments;
4pub mod board;
5pub mod bot;
6pub mod broadcasts;
7pub mod bulk_pairings;
8pub mod challenges;
9pub mod external_engine;
10pub mod fide;
11pub mod games;
12pub mod messaging;
13#[cfg(feature = "oauth")]
14pub mod oauth;
15pub mod openings;
16pub mod puzzles;
17pub mod relations;
18pub mod simuls;
19pub mod studies;
20pub mod swiss_tournaments;
21pub mod tablebase;
22pub mod teams;
23pub mod tv;
24pub mod users;
25
26use crate::error;
27use serde::{Deserialize, Serialize, de::DeserializeOwned};
28use serde_with::skip_serializing_none;
29
30pub trait BodyBounds: Serialize {}
31impl<B: Serialize> BodyBounds for B {}
32
33pub trait QueryBounds: Serialize + Default {}
34impl<Q: Serialize + Default> QueryBounds for Q {}
35
36pub trait ModelBounds: DeserializeOwned {}
37impl<M: DeserializeOwned> ModelBounds for M {}
38
39#[derive(Default, Clone, Debug)]
40pub enum Body<B: BodyBounds> {
41    Form(B),
42    Json(B),
43    PlainText(String),
44    #[default]
45    Empty,
46}
47
48impl<B: BodyBounds> Body<B> {
49    fn as_mime(&self) -> Option<mime::Mime> {
50        match &self {
51            Body::Form(_) => Some(mime::APPLICATION_WWW_FORM_URLENCODED),
52            Body::Json(_) => Some(mime::APPLICATION_JSON),
53            Body::PlainText(_) => Some(mime::TEXT_PLAIN),
54            Body::Empty => None,
55        }
56    }
57
58    fn as_encoded_string(&self) -> error::Result<String> {
59        let body = match &self {
60            Body::Form(form) => to_form_string(&form)?,
61            Body::Json(json) => to_json_string(&json)?,
62            Body::PlainText(text) => text.to_string(),
63            Body::Empty => "".to_string(),
64        };
65        Ok(body)
66    }
67}
68
69#[derive(Clone, Copy, Debug, Default)]
70pub enum Domain {
71    #[default]
72    Lichess,
73    Tablebase,
74    Engine,
75    Explorer,
76}
77
78impl AsRef<str> for Domain {
79    fn as_ref(&self) -> &str {
80        match self {
81            Domain::Lichess => "lichess.org",
82            Domain::Tablebase => "tablebase.lichess.ovh",
83            Domain::Engine => "engine.lichess.ovh",
84            Domain::Explorer => "explorer.lichess.ovh",
85        }
86    }
87}
88
89#[derive(Clone, Debug)]
90pub struct Request<Q, B = ()>
91where
92    Q: QueryBounds,
93    B: BodyBounds,
94{
95    pub(crate) domain: Domain,
96    pub(crate) method: http::Method,
97    pub(crate) path: String,
98    pub(crate) query: Option<Q>,
99    pub(crate) body: Body<B>,
100}
101
102impl<Q, B> Request<Q, B>
103where
104    Q: QueryBounds + Default,
105    B: BodyBounds,
106{
107    pub(crate) fn create(
108        path: impl Into<String>,
109        query: impl Into<Option<Q>>,
110        body: impl Into<Option<Body<B>>>,
111        domain: impl Into<Option<Domain>>,
112        method: http::Method,
113    ) -> Self {
114        Self {
115            domain: domain.into().unwrap_or_default(),
116            method,
117            path: path.into(),
118            query: query.into(),
119            body: body.into().unwrap_or_default(),
120        }
121    }
122
123    pub(crate) fn get(
124        path: impl Into<String>,
125        query: impl Into<Option<Q>>,
126        domain: impl Into<Option<Domain>>,
127    ) -> Self {
128        Self::create(path, query, None, domain, http::Method::GET)
129    }
130
131    pub(crate) fn head(
132        path: impl Into<String>,
133        query: impl Into<Option<Q>>,
134        domain: impl Into<Option<Domain>>,
135    ) -> Self {
136        Self::create(path, query, None, domain, http::Method::HEAD)
137    }
138
139    pub(crate) fn post(
140        path: impl Into<String>,
141        query: impl Into<Option<Q>>,
142        body: impl Into<Option<Body<B>>>,
143        domain: impl Into<Option<Domain>>,
144    ) -> Self {
145        Self::create(path, query, body, domain, http::Method::POST)
146    }
147
148    pub(crate) fn put(
149        path: impl Into<String>,
150        query: impl Into<Option<Q>>,
151        body: impl Into<Option<Body<B>>>,
152        domain: impl Into<Option<Domain>>,
153    ) -> Self {
154        Self::create(path, query, body, domain, http::Method::PUT)
155    }
156
157    pub(crate) fn delete(
158        path: impl Into<String>,
159        query: impl Into<Option<Q>>,
160        body: impl Into<Option<Body<B>>>,
161        domain: Option<Domain>,
162    ) -> Self {
163        Self::create(path, query, body, domain, http::Method::DELETE)
164    }
165}
166
167impl<Q, B> Request<Q, B>
168where
169    Q: QueryBounds,
170    B: BodyBounds,
171{
172    pub(crate) fn into_http_request(
173        self,
174        accept: &str,
175    ) -> error::Result<http::Request<bytes::Bytes>> {
176        make_request(
177            self.domain,
178            self.method,
179            self.path,
180            self.query,
181            self.body,
182            accept,
183        )
184    }
185}
186
187fn make_request<Q, B>(
188    domain: Domain,
189    method: http::Method,
190    path: String,
191    query: Option<Q>,
192    body: Body<B>,
193    accept: &str,
194) -> error::Result<http::Request<bytes::Bytes>>
195where
196    Q: QueryBounds,
197    B: BodyBounds,
198{
199    let mut builder = http::Request::builder();
200
201    if let Some(mime) = body.as_mime() {
202        builder = builder.header(http::header::CONTENT_TYPE, mime.to_string());
203    }
204    let accept_header = http::HeaderValue::from_str(accept)
205        .map_err(|e| error::Error::HttpRequestBuilder(http::Error::from(e)))?;
206    builder = builder.header(http::header::ACCEPT, accept_header);
207
208    let url = make_url(domain, path, query)?;
209    let body = bytes::Bytes::from(body.as_encoded_string()?);
210
211    let request = builder
212        .method(method)
213        .uri(url.as_str())
214        .body(body)
215        .map_err(error::Error::HttpRequestBuilder)?;
216
217    Ok(request)
218}
219
220fn make_url<Q>(domain: Domain, path: String, query: Option<Q>) -> error::Result<url::Url>
221where
222    Q: QueryBounds,
223{
224    let base_url = format!("https://{}", domain.as_ref());
225    let mut url = url::Url::parse(&base_url).expect("invalid base url");
226
227    if let Some(query) = query {
228        let mut query_pairs = url.query_pairs_mut();
229        let query_serializer = serde_urlencoded::Serializer::new(&mut query_pairs);
230        query.serialize(query_serializer)?;
231    }
232
233    url.set_path(&path.to_string());
234
235    Ok(url)
236}
237
238fn to_json_string<B: BodyBounds>(body: &B) -> error::Result<String> {
239    serde_json::to_string(&body).map_err(error::Error::Json)
240}
241
242fn to_form_string<B: BodyBounds>(body: &B) -> error::Result<String> {
243    serde_urlencoded::to_string(body).map_err(error::Error::UrlEncoded)
244}
245
246#[derive(Clone, Debug, Serialize, Deserialize)]
247pub struct Ok {
248    pub ok: bool,
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize)]
252#[serde(untagged)]
253pub enum Response<M> {
254    Model(M),
255    Error { error: String },
256}
257
258#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
259#[serde(rename_all = "lowercase")]
260pub enum Color {
261    #[default]
262    White,
263    Black,
264    Random,
265}
266
267#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
268#[serde(rename_all = "lowercase")]
269pub enum PlayerColor {
270    #[default]
271    White,
272    Black,
273}
274
275#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
276#[serde(rename_all = "camelCase")]
277pub enum Speed {
278    UltraBullet,
279    Bullet,
280    Blitz,
281    Rapid,
282    Classical,
283    Correspondence,
284}
285
286#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Hash)]
287#[serde(rename_all = "camelCase")]
288pub enum PerfType {
289    UltraBullet,
290    Bullet,
291    Blitz,
292    Rapid,
293    Classical,
294    Correspondence,
295    Chess960,
296    Crazyhouse,
297    Antichess,
298    Atomic,
299    Horde,
300    KingOfTheHill,
301    RacingKings,
302    ThreeCheck,
303}
304
305impl PerfType {
306    pub fn as_str(&self) -> &'static str {
307        match self {
308            Self::UltraBullet => "ultraBullet",
309            Self::Bullet => "bullet",
310            Self::Blitz => "blitz",
311            Self::Rapid => "rapid",
312            Self::Classical => "classical",
313            Self::Correspondence => "correspondence",
314            Self::Chess960 => "chess960",
315            Self::Crazyhouse => "crazyhouse",
316            Self::Antichess => "antichess",
317            Self::Atomic => "atomic",
318            Self::Horde => "horde",
319            Self::KingOfTheHill => "kingOfTheHill",
320            Self::RacingKings => "racingKings",
321            Self::ThreeCheck => "threeCheck",
322        }
323    }
324}
325
326impl std::fmt::Display for PerfType {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        f.write_str(self.as_str())
329    }
330}
331
332#[skip_serializing_none]
333#[derive(Clone, Debug, Deserialize, Serialize)]
334pub struct LightUser {
335    pub id: String,
336    pub name: String,
337    pub title: Option<Title>,
338    pub flair: Option<String>,
339    pub patron: Option<bool>,
340    #[serde(rename = "patronColor")]
341    pub patron_color: Option<u8>,
342    pub online: Option<bool>,
343}
344
345#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
346#[serde(rename_all = "UPPERCASE")]
347pub enum Title {
348    Gm,
349    Wgm,
350    Im,
351    Wim,
352    Fm,
353    Wfm,
354    Nm,
355    Cm,
356    Wcm,
357    Wnm,
358    Lm,
359    Bot,
360}
361
362#[skip_serializing_none]
363#[derive(Clone, Debug, Deserialize, Serialize)]
364pub struct Variant {
365    pub key: VariantKey,
366    pub name: String,
367    pub short: Option<String>,
368    pub icon: Option<String>,
369}
370
371#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
372#[serde(rename_all = "camelCase")]
373pub enum VariantKey {
374    #[default]
375    Standard,
376    Chess960,
377    Crazyhouse,
378    Antichess,
379    Atomic,
380    Horde,
381    KingOfTheHill,
382    RacingKings,
383    ThreeCheck,
384    FromPosition,
385}
386
387#[derive(Default, Clone, Debug, Serialize, PartialEq, Eq, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub enum Room {
390    #[default]
391    Player,
392    Spectator,
393}
394
395#[derive(Clone, Debug, Deserialize, Serialize)]
396pub struct GameCompat {
397    pub bot: Option<bool>,
398    pub board: Option<bool>,
399}
400
401#[serde_with::skip_serializing_none]
402#[derive(Clone, Debug, Serialize, Deserialize)]
403#[serde(rename_all = "camelCase")]
404pub struct Clock {
405    pub initial: u32,
406    pub increment: u32,
407    pub total_time: Option<u32>,
408}
409
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411pub enum Days {
412    One,
413    Two,
414    Three,
415    Five,
416    Seven,
417    Ten,
418    Fourteen,
419}
420
421impl Serialize for Days {
422    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
423    where
424        S: serde::Serializer,
425    {
426        let value: u32 = (*self).into();
427        value.serialize(serializer)
428    }
429}
430
431impl<'de> Deserialize<'de> for Days {
432    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
433    where
434        D: serde::Deserializer<'de>,
435    {
436        let value = u32::deserialize(deserializer)?;
437        Ok(Days::from(value))
438    }
439}
440
441impl From<u32> for Days {
442    fn from(value: u32) -> Self {
443        match value {
444            1 => Days::One,
445            2 => Days::Two,
446            3 => Days::Three,
447            5 => Days::Five,
448            7 => Days::Seven,
449            10 => Days::Ten,
450            14 => Days::Fourteen,
451            _ => panic!("Invalid days {}", value),
452        }
453    }
454}
455
456impl From<Days> for u32 {
457    fn from(value: Days) -> Self {
458        match value {
459            Days::One => 1,
460            Days::Two => 2,
461            Days::Three => 3,
462            Days::Five => 5,
463            Days::Seven => 7,
464            Days::Ten => 10,
465            Days::Fourteen => 14,
466        }
467    }
468}
469
470#[skip_serializing_none]
471#[derive(Clone, Debug, Deserialize, Serialize)]
472#[serde(rename_all = "camelCase")]
473pub struct ArenaTournament {
474    pub id: String,
475    pub created_by: String,
476    pub system: String,
477    pub minutes: u32,
478    pub clock: ArenaClock,
479    pub rated: bool,
480    pub full_name: String,
481    pub nb_players: u32,
482    pub variant: Variant,
483    pub starts_at: i64,
484    pub finishes_at: i64,
485    pub status: ArenaStatus,
486    pub perf: ArenaPerf,
487    pub seconds_to_start: Option<i32>,
488    pub has_max_rating: Option<bool>,
489    pub max_rating: Option<ArenaRatingObj>,
490    pub min_rating: Option<ArenaRatingObj>,
491    pub min_rated_games: Option<ArenaMinRatedGames>,
492    pub bots_allowed: Option<bool>,
493    pub min_account_age_in_days: Option<i32>,
494    pub only_titled: Option<bool>,
495    pub team_member: Option<String>,
496    pub private: Option<bool>,
497    pub position: Option<ArenaPosition>,
498    pub schedule: Option<ArenaSchedule>,
499    pub team_battle: Option<ArenaTeamBattle>,
500    pub winner: Option<LightUser>,
501}
502
503#[derive(Clone, Debug, Deserialize, Serialize)]
504pub struct ArenaClock {
505    pub limit: u32,
506    pub increment: u32,
507}
508
509#[derive(Clone, Copy, Debug, PartialEq, Eq)]
510pub enum ArenaStatus {
511    Created,
512    Started,
513    Finished,
514}
515
516impl Serialize for ArenaStatus {
517    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
518    where
519        S: serde::Serializer,
520    {
521        let value: u8 = match self {
522            ArenaStatus::Created => 10,
523            ArenaStatus::Started => 20,
524            ArenaStatus::Finished => 30,
525        };
526        value.serialize(serializer)
527    }
528}
529
530impl<'de> Deserialize<'de> for ArenaStatus {
531    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
532    where
533        D: serde::Deserializer<'de>,
534    {
535        let value = u8::deserialize(deserializer)?;
536        match value {
537            10 => Ok(ArenaStatus::Created),
538            20 => Ok(ArenaStatus::Started),
539            30 => Ok(ArenaStatus::Finished),
540            _ => Err(serde::de::Error::custom(format!(
541                "invalid arena tournament status {value}"
542            ))),
543        }
544    }
545}
546
547#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
548#[serde(rename_all = "lowercase")]
549pub enum ArenaStatusName {
550    Created,
551    Started,
552    Finished,
553}
554
555#[skip_serializing_none]
556#[derive(Clone, Debug, Deserialize, Serialize)]
557pub struct ArenaPerf {
558    pub key: PerfType,
559    pub name: String,
560    pub position: i32,
561    pub icon: Option<String>,
562}
563
564#[skip_serializing_none]
565#[derive(Clone, Debug, Deserialize, Serialize)]
566pub struct ArenaRatingObj {
567    pub perf: Option<PerfType>,
568    pub rating: i32,
569}
570
571#[derive(Clone, Debug, Deserialize, Serialize)]
572pub struct ArenaMinRatedGames {
573    pub nb: Option<i32>,
574}
575
576#[derive(Clone, Debug, Deserialize, Serialize)]
577#[serde(untagged)]
578pub enum ArenaPosition {
579    Thematic {
580        eco: String,
581        name: String,
582        fen: String,
583        url: String,
584    },
585    Custom {
586        name: String,
587        fen: String,
588    },
589}
590
591#[skip_serializing_none]
592#[derive(Clone, Debug, Deserialize, Serialize)]
593pub struct ArenaSchedule {
594    pub freq: Option<String>,
595    pub speed: Option<String>,
596}
597
598#[skip_serializing_none]
599#[derive(Clone, Debug, Deserialize, Serialize)]
600pub struct ArenaTeamBattle {
601    pub teams: Option<Vec<String>>,
602    pub nb_leaders: Option<i32>,
603}
604
605#[skip_serializing_none]
606#[derive(Clone, Debug, Deserialize, Serialize)]
607#[serde(rename_all = "camelCase")]
608pub struct SwissTournament {
609    pub id: String,
610    pub created_by: String,
611    pub starts_at: String,
612    pub name: String,
613    pub clock: SwissClock,
614    pub variant: VariantKey,
615    pub round: i32,
616    pub nb_rounds: i32,
617    pub nb_players: i32,
618    pub nb_ongoing: i32,
619    pub status: SwissStatus,
620    pub stats: Option<SwissStats>,
621    pub rated: bool,
622    pub verdicts: Verdicts,
623    pub next_round: Option<SwissNextRound>,
624}
625
626#[derive(Clone, Debug, Deserialize, Serialize)]
627pub struct SwissClock {
628    pub limit: i32,
629    pub increment: i32,
630}
631
632#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
633#[serde(rename_all = "lowercase")]
634pub enum SwissStatus {
635    Created,
636    Started,
637    Finished,
638}
639
640#[derive(Clone, Debug, Deserialize, Serialize)]
641#[serde(rename_all = "camelCase")]
642pub struct SwissStats {
643    pub games: i32,
644    pub white_wins: i32,
645    pub black_wins: i32,
646    pub draws: i32,
647    pub byes: i32,
648    pub absences: i32,
649    pub average_rating: f64,
650}
651
652#[derive(Clone, Debug, Deserialize, Serialize)]
653pub struct SwissNextRound {
654    pub at: String,
655    #[serde(rename = "in")]
656    pub in_seconds: i32,
657}
658
659#[derive(Clone, Debug, Deserialize, Serialize)]
660pub struct Verdicts {
661    pub accepted: bool,
662    pub list: Vec<Verdict>,
663}
664
665#[derive(Clone, Debug, Deserialize, Serialize)]
666pub struct Verdict {
667    pub condition: String,
668    pub verdict: String,
669}