use std::sync::Arc;
use std::time::Duration;
use reqwest::Method;
use url::Url;
use crate::config::{Config, Host};
use crate::error::Result;
use crate::http::ApiRequest;
use crate::retry::RetryPolicy;
use crate::secret::Secret;
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_mins(5);
#[derive(Debug, Clone)]
pub struct LichessClient {
http: reqwest::Client,
config: Arc<Config>,
}
impl LichessClient {
#[must_use]
pub fn new() -> Self {
Self::builder()
.build()
.expect("default Lichess client should always build")
}
#[must_use]
pub fn builder() -> LichessClientBuilder {
LichessClientBuilder::default()
}
pub(crate) fn request(&self, method: Method, host: Host, path: &str) -> ApiRequest {
let url = self.config.url(host, path);
let builder = self.http.request(method, url);
let builder = match &self.config.token {
Some(token) => builder.bearer_auth(token.expose()),
None => builder,
};
ApiRequest::new(builder, self.config.retry_policy)
}
pub(crate) fn absolute_url(&self, host: Host, path: &str) -> String {
self.config.url(host, path)
}
pub(crate) fn max_line_bytes(&self) -> usize {
self.config.max_line_bytes
}
}
impl Default for LichessClient {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct LichessClientBuilder {
config: Config,
http: Option<reqwest::Client>,
connect_timeout: Option<Duration>,
read_timeout: Option<Duration>,
}
impl LichessClientBuilder {
#[must_use]
pub fn token(mut self, token: impl Into<String>) -> Self {
self.config.token = Some(Secret::new(token.into()));
self
}
#[must_use]
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.config.user_agent = user_agent.into();
self
}
#[must_use]
pub fn http_client(mut self, http: reqwest::Client) -> Self {
self.http = Some(http);
self
}
#[must_use]
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = Some(timeout);
self
}
#[must_use]
pub fn read_timeout(mut self, timeout: Duration) -> Self {
self.read_timeout = Some(timeout);
self
}
#[must_use]
pub fn max_line_bytes(mut self, max: usize) -> Self {
self.config.max_line_bytes = max;
self
}
#[must_use]
pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
self.config.retry_policy = policy;
self
}
#[must_use]
pub fn base_url(mut self, url: &Url) -> Self {
self.config.set_base(Host::Default, url);
self
}
#[must_use]
pub fn opening_explorer_url(mut self, url: &Url) -> Self {
self.config.set_base(Host::OpeningExplorer, url);
self
}
#[must_use]
pub fn tablebase_url(mut self, url: &Url) -> Self {
self.config.set_base(Host::Tablebase, url);
self
}
#[must_use]
pub fn engine_url(mut self, url: &Url) -> Self {
self.config.set_base(Host::Engine, url);
self
}
pub fn build(self) -> Result<LichessClient> {
let http = match self.http {
Some(http) => http,
None => reqwest::Client::builder()
.user_agent(&self.config.user_agent)
.connect_timeout(self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT))
.read_timeout(self.read_timeout.unwrap_or(DEFAULT_READ_TIMEOUT))
.build()?,
};
Ok(LichessClient {
http,
config: Arc::new(self.config),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_with_custom_read_timeout() {
let client = LichessClient::builder()
.read_timeout(Duration::from_secs(10))
.build();
assert!(client.is_ok());
}
#[test]
fn max_line_bytes_setter_overrides_the_default() {
let client = LichessClient::builder()
.max_line_bytes(1024)
.build()
.unwrap();
assert_eq!(client.max_line_bytes(), 1024);
}
#[test]
fn builds_with_token_and_connect_timeout() {
let client = LichessClient::builder()
.token("lip_test")
.connect_timeout(Duration::from_secs(5))
.build();
assert!(client.is_ok());
}
#[test]
fn anonymous_request_has_no_bearer_header() {
let client = LichessClient::new();
let request = client
.request(Method::GET, Host::Default, "/api/account")
.build()
.unwrap();
assert!(
request
.headers()
.get(reqwest::header::AUTHORIZATION)
.is_none()
);
}
#[test]
fn authenticated_request_sets_bearer_header() {
let client = LichessClient::builder().token("lip_test").build().unwrap();
let request = client
.request(Method::GET, Host::Default, "/api/account")
.build()
.unwrap();
let auth = request
.headers()
.get(reqwest::header::AUTHORIZATION)
.unwrap();
assert_eq!(auth, "Bearer lip_test");
}
}