use futures_util::stream::BoxStream;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::client::LichessClient;
use crate::config::Host;
use crate::error::Result;
use crate::http;
use crate::model::LichessColor;
#[derive(Debug, Serialize)]
struct ExplorerQuery<'a> {
fen: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
play: Option<&'a str>,
}
#[derive(Debug, Serialize)]
struct PlayerQuery<'a> {
player: &'a str,
color: &'a str,
fen: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
play: Option<&'a str>,
}
#[derive(Debug)]
pub struct OpeningExplorerApi<'a> {
client: &'a LichessClient,
}
impl<'a> OpeningExplorerApi<'a> {
pub(crate) fn new(client: &'a LichessClient) -> Self {
Self { client }
}
pub async fn masters(&self, fen: &str, play: Option<&str>) -> Result<LichessExplorerResult> {
self.lookup("/masters", fen, play).await
}
pub async fn lichess(&self, fen: &str, play: Option<&str>) -> Result<LichessExplorerResult> {
self.lookup("/lichess", fen, play).await
}
pub async fn player(
&self,
player: &str,
color: &str,
fen: &str,
play: Option<&str>,
) -> Result<BoxStream<'static, Result<LichessExplorerResult>>> {
let query = PlayerQuery {
player,
color,
fen,
play,
};
let request = self
.client
.request(Method::GET, Host::OpeningExplorer, "/player")
.query(&query);
http::stream(request, self.client.max_line_bytes()).await
}
pub async fn masters_pgn(&self, game_id: &str) -> Result<String> {
let path = format!("/masters/pgn/{}", http::segment(game_id));
let request = self
.client
.request(Method::GET, Host::OpeningExplorer, &path);
http::text(request).await
}
async fn lookup(
&self,
path: &str,
fen: &str,
play: Option<&str>,
) -> Result<LichessExplorerResult> {
let request = self
.client
.request(Method::GET, Host::OpeningExplorer, path)
.query(&ExplorerQuery { fen, play });
http::json(request, "LichessExplorerResult").await
}
}
impl LichessClient {
#[must_use]
pub fn opening_explorer(&self) -> OpeningExplorerApi<'_> {
OpeningExplorerApi::new(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessExplorerOpening {
pub eco: String,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessExplorerGamePlayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rating: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessExplorerGame {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uci: Option<String>,
#[serde(default)]
pub winner: Option<LichessColor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub white: Option<LichessExplorerGamePlayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub black: Option<LichessExplorerGamePlayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub year: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub month: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessExplorerMove {
pub uci: String,
pub san: String,
pub white: u64,
pub draws: u64,
pub black: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub average_rating: Option<u32>,
#[serde(default)]
pub game: Option<LichessExplorerGame>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessExplorerResult {
#[serde(default)]
pub opening: Option<LichessExplorerOpening>,
pub white: u64,
pub draws: u64,
pub black: u64,
#[serde(default)]
pub moves: Vec<LichessExplorerMove>,
#[serde(default)]
pub top_games: Vec<LichessExplorerGame>,
#[serde(default)]
pub recent_games: Vec<LichessExplorerGame>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_explorer_result() {
let json = r#"{"opening":{"eco":"B01","name":"Scandinavian"},
"white":100,"draws":40,"black":60,
"moves":[{"uci":"e2e4","san":"e4","white":50,"draws":20,"black":30,
"averageRating":2400}],
"topGames":[{"id":"g","uci":"e2e4","winner":"white",
"white":{"name":"A","rating":2700},"year":2020}]}"#;
let result: LichessExplorerResult = serde_json::from_str(json).unwrap();
assert_eq!(result.white, 100);
assert_eq!(result.moves[0].average_rating, Some(2400));
assert_eq!(result.top_games[0].winner, Some(LichessColor::White));
}
}