lichess_api/model/
mod.rs

1pub mod account;
2pub mod analysis;
3pub mod board;
4pub mod bot;
5pub mod challenges;
6pub mod external_engine;
7pub mod fide;
8pub mod games;
9pub mod messaging;
10pub mod oauth;
11pub mod openings;
12pub mod puzzles;
13pub mod relations;
14pub mod simuls;
15pub mod studies;
16pub mod tablebase;
17pub mod tv;
18pub mod users;
19
20use crate::error;
21use serde::{Deserialize, Serialize, de::DeserializeOwned};
22use serde_with::skip_serializing_none;
23
24pub trait BodyBounds: Serialize {}
25impl<B: Serialize> BodyBounds for B {}
26
27pub trait QueryBounds: Serialize + Default {}
28impl<Q: Serialize + Default> QueryBounds for Q {}
29
30pub trait ModelBounds: DeserializeOwned {}
31impl<M: DeserializeOwned> ModelBounds for M {}
32
33#[derive(Default, Clone, Debug)]
34pub enum Body<B: BodyBounds> {
35    Form(B),
36    Json(B),
37    PlainText(String),
38    #[default]
39    Empty,
40}
41
42impl<B: BodyBounds> Body<B> {
43    fn as_mime(&self) -> Option<mime::Mime> {
44        match &self {
45            Body::Form(_) => Some(mime::APPLICATION_WWW_FORM_URLENCODED),
46            Body::Json(_) => Some(mime::APPLICATION_JSON),
47            Body::PlainText(_) => Some(mime::TEXT_PLAIN),
48            Body::Empty => None,
49        }
50    }
51
52    fn as_encoded_string(&self) -> error::Result<String> {
53        let body = match &self {
54            Body::Form(form) => to_form_string(&form)?,
55            Body::Json(json) => to_json_string(&json)?,
56            Body::PlainText(text) => text.to_string(),
57            Body::Empty => "".to_string(),
58        };
59        Ok(body)
60    }
61}
62
63#[derive(Clone, Copy, Debug, Default)]
64pub enum Domain {
65    #[default]
66    Lichess,
67    Tablebase,
68    Engine,
69    Explorer,
70}
71
72impl AsRef<str> for Domain {
73    fn as_ref(&self) -> &str {
74        match self {
75            Domain::Lichess => "lichess.org",
76            Domain::Tablebase => "tablebase.lichess.ovh",
77            Domain::Engine => "engine.lichess.ovh",
78            Domain::Explorer => "explorer.lichess.ovh",
79        }
80    }
81}
82
83#[derive(Clone, Debug)]
84pub struct Request<Q, B = ()>
85where
86    Q: QueryBounds,
87    B: BodyBounds,
88{
89    pub(crate) domain: Domain,
90    pub(crate) method: http::Method,
91    pub(crate) path: String,
92    pub(crate) query: Option<Q>,
93    pub(crate) body: Body<B>,
94}
95
96impl<Q, B> Request<Q, B>
97where
98    Q: QueryBounds + Default,
99    B: BodyBounds,
100{
101    pub(crate) fn create(
102        path: impl Into<String>,
103        query: impl Into<Option<Q>>,
104        body: impl Into<Option<Body<B>>>,
105        domain: impl Into<Option<Domain>>,
106        method: http::Method,
107    ) -> Self {
108        Self {
109            domain: domain.into().unwrap_or_default(),
110            method,
111            path: path.into(),
112            query: query.into(),
113            body: body.into().unwrap_or_default(),
114        }
115    }
116
117    pub(crate) fn get(
118        path: impl Into<String>,
119        query: impl Into<Option<Q>>,
120        domain: impl Into<Option<Domain>>,
121    ) -> Self {
122        Self::create(path, query, None, domain, http::Method::GET)
123    }
124
125    pub(crate) fn post(
126        path: impl Into<String>,
127        query: impl Into<Option<Q>>,
128        body: impl Into<Option<Body<B>>>,
129        domain: impl Into<Option<Domain>>,
130    ) -> Self {
131        Self::create(path, query, body, domain, http::Method::POST)
132    }
133
134    pub(crate) fn put(
135        path: impl Into<String>,
136        query: impl Into<Option<Q>>,
137        body: impl Into<Option<Body<B>>>,
138        domain: impl Into<Option<Domain>>,
139    ) -> Self {
140        Self::create(path, query, body, domain, http::Method::PUT)
141    }
142
143    pub(crate) fn delete(
144        path: impl Into<String>,
145        query: impl Into<Option<Q>>,
146        body: impl Into<Option<Body<B>>>,
147        domain: Option<Domain>,
148    ) -> Self {
149        Self::create(path, query, body, domain, http::Method::DELETE)
150    }
151}
152
153impl<Q, B> Request<Q, B>
154where
155    Q: QueryBounds,
156    B: BodyBounds,
157{
158    pub(crate) fn as_http_request(
159        self,
160        accept: &str,
161    ) -> error::Result<http::Request<bytes::Bytes>> {
162        make_request(
163            self.domain,
164            self.method,
165            self.path,
166            self.query,
167            self.body,
168            accept,
169        )
170    }
171}
172
173fn make_request<Q, B>(
174    domain: Domain,
175    method: http::Method,
176    path: String,
177    query: Option<Q>,
178    body: Body<B>,
179    accept: &str,
180) -> error::Result<http::Request<bytes::Bytes>>
181where
182    Q: QueryBounds,
183    B: BodyBounds,
184{
185    let mut builder = http::Request::builder();
186
187    if let Some(mime) = body.as_mime() {
188        builder = builder.header(http::header::CONTENT_TYPE, mime.to_string());
189    }
190    let accept_header = http::HeaderValue::from_str(accept)
191        .map_err(|e| error::Error::HttpRequestBuilder(http::Error::from(e)))?;
192    builder = builder.header(http::header::ACCEPT, accept_header);
193
194    let url = make_url(domain, path, query)?;
195    let body = bytes::Bytes::from(body.as_encoded_string()?);
196
197    let request = builder
198        .method(method)
199        .uri(url.as_str())
200        .body(body)
201        .map_err(|e| error::Error::HttpRequestBuilder(e))?;
202
203    Ok(request)
204}
205
206fn make_url<Q>(domain: Domain, path: String, query: Option<Q>) -> error::Result<url::Url>
207where
208    Q: QueryBounds,
209{
210    let base_url = format!("https://{}", domain.as_ref());
211    let mut url = url::Url::parse(&base_url).expect("invalid base url");
212
213    if let Some(query) = query {
214        let mut query_pairs = url.query_pairs_mut();
215        let query_serializer = serde_urlencoded::Serializer::new(&mut query_pairs);
216        query.serialize(query_serializer)?;
217    }
218
219    url.set_path(&path.to_string());
220
221    Ok(url)
222}
223
224fn to_json_string<B: BodyBounds>(body: &B) -> error::Result<String> {
225    serde_json::to_string(&body).map_err(|e| error::Error::Json(e))
226}
227
228fn to_form_string<B: BodyBounds>(body: &B) -> error::Result<String> {
229    serde_urlencoded::to_string(&body).map_err(|e| error::Error::UrlEncoded(e))
230}
231
232#[derive(Clone, Debug, Serialize, Deserialize)]
233pub struct Ok {
234    pub ok: bool,
235}
236
237#[derive(Clone, Debug, Serialize, Deserialize)]
238#[serde(untagged)]
239pub enum Response<M> {
240    Model(M),
241    Error { error: String },
242}
243
244#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
245#[serde(rename_all = "lowercase")]
246pub enum Color {
247    #[default]
248    White,
249    Black,
250    Random,
251}
252
253#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
254#[serde(rename_all = "lowercase")]
255pub enum PlayerColor {
256    #[default]
257    White,
258    Black,
259}
260
261#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
262#[serde(rename_all = "camelCase")]
263pub enum Speed {
264    UltraBullet,
265    Bullet,
266    Blitz,
267    Rapid,
268    Classical,
269    Correspondence,
270}
271
272#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Hash)]
273#[serde(rename_all = "camelCase")]
274pub enum PerfType {
275    UltraBullet,
276    Bullet,
277    Blitz,
278    Rapid,
279    Classical,
280    Chess960,
281    Crazyhouse,
282    Antichess,
283    Atomic,
284    Horde,
285    KingOfTheHill,
286    RacingKings,
287    ThreeCheck,
288}
289
290impl PerfType {
291    pub fn as_str(&self) -> &'static str {
292        match self {
293            Self::UltraBullet => "ultraBullet",
294            Self::Bullet => "bullet",
295            Self::Blitz => "blitz",
296            Self::Rapid => "rapid",
297            Self::Classical => "classical",
298            Self::Chess960 => "chess960",
299            Self::Crazyhouse => "crazyhouse",
300            Self::Antichess => "antichess",
301            Self::Atomic => "atomic",
302            Self::Horde => "horde",
303            Self::KingOfTheHill => "kingOfTheHill",
304            Self::RacingKings => "racingKings",
305            Self::ThreeCheck => "threeCheck",
306        }
307    }
308}
309
310impl std::fmt::Display for PerfType {
311    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312        f.write_str(self.as_str())
313    }
314}
315
316#[skip_serializing_none]
317#[derive(Clone, Debug, Deserialize, Serialize)]
318pub struct LightUser {
319    pub id: String,
320    pub name: String,
321    pub title: Option<Title>,
322    pub flair: Option<String>,
323    pub patron: Option<bool>,
324    pub online: Option<bool>,
325}
326
327#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
328#[serde(rename_all = "UPPERCASE")]
329pub enum Title {
330    Gm,
331    Wgm,
332    Im,
333    Wim,
334    Fm,
335    Wfm,
336    Nm,
337    Cm,
338    Wcm,
339    Wnm,
340    Lm,
341    Bot,
342}
343
344#[skip_serializing_none]
345#[derive(Clone, Debug, Deserialize, Serialize)]
346pub struct Variant {
347    pub key: VariantKey,
348    pub name: String,
349    pub short: Option<String>,
350    pub icon: Option<String>,
351}
352
353#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
354#[serde(rename_all = "camelCase")]
355pub enum VariantKey {
356    #[default]
357    Standard,
358    Chess960,
359    Crazyhouse,
360    Antichess,
361    Atomic,
362    Horde,
363    KingOfTheHill,
364    RacingKings,
365    ThreeCheck,
366    FromPosition,
367}
368
369#[derive(Default, Clone, Debug, Serialize, PartialEq, Eq, Deserialize)]
370#[serde(rename_all = "camelCase")]
371pub enum Room {
372    #[default]
373    Player,
374    Spectator,
375}
376
377#[derive(Clone, Debug, Deserialize, Serialize)]
378pub struct GameCompat {
379    pub bot: Option<bool>,
380    pub board: Option<bool>,
381}
382
383#[serde_with::skip_serializing_none]
384#[derive(Clone, Debug, Serialize, Deserialize)]
385#[serde(rename_all = "camelCase")]
386pub struct Clock {
387    pub initial: u32,
388    pub increment: u32,
389    pub total_time: Option<u32>,
390}
391
392#[derive(Clone, Copy, Debug, PartialEq, Eq)]
393pub enum Days {
394    One,
395    Two,
396    Three,
397    Five,
398    Seven,
399    Ten,
400    Fourteen,
401}
402
403impl Serialize for Days {
404    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
405    where
406        S: serde::Serializer,
407    {
408        let value: u32 = (*self).into();
409        value.serialize(serializer)
410    }
411}
412
413impl<'de> Deserialize<'de> for Days {
414    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
415    where
416        D: serde::Deserializer<'de>,
417    {
418        let value = u32::deserialize(deserializer)?;
419        Ok(Days::from(value))
420    }
421}
422
423impl From<u32> for Days {
424    fn from(value: u32) -> Self {
425        match value {
426            1 => Days::One,
427            2 => Days::Two,
428            3 => Days::Three,
429            5 => Days::Five,
430            7 => Days::Seven,
431            10 => Days::Ten,
432            14 => Days::Fourteen,
433            _ => panic!("Invalid days {}", value),
434        }
435    }
436}
437
438impl Into<u32> for Days {
439    fn into(self) -> u32 {
440        match self {
441            Days::One => 1,
442            Days::Two => 2,
443            Days::Three => 3,
444            Days::Five => 5,
445            Days::Seven => 7,
446            Days::Ten => 10,
447            Days::Fourteen => 14,
448        }
449    }
450}