use crate::folders::FolderClient;
use crate::system::SystemClient;
use crate::users::{UserClient, UserName, UsersClient};
use crate::utils::number_from_string;
use crate::{Error, Result};
use reqwest::{Method, Url};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct Client {
pub(crate) base_url: Arc<Url>,
api: reqwest::Client,
}
#[derive(Debug)]
pub struct ClientBuilder {
base_url: Arc<Url>,
timeout: Duration,
ssl_verify: bool,
html5: u8,
}
impl ClientBuilder {
fn new(base_url: impl AsRef<str>) -> Result<Self> {
let url = Url::parse(base_url.as_ref())
.map_err(|e| Error::Client(format!("Invalid URL: {e}")))?;
Ok(Self {
base_url: Arc::new(url),
timeout: Duration::from_secs(30),
ssl_verify: true,
html5: 1,
})
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn ssl_verify(mut self, ssl_verify: bool) -> Self {
self.ssl_verify = ssl_verify;
self
}
pub fn html5(mut self, html5: u8) -> Self {
self.html5 = html5;
self
}
pub async fn login(
&self,
username: impl Into<String>,
password: impl Into<String>,
) -> Result<Client> {
let client = Client {
base_url: self.base_url.clone(),
api: reqwest::Client::builder()
.cookie_store(true)
.timeout(self.timeout)
.danger_accept_invalid_certs(!self.ssl_verify)
.build()?,
};
let username = UserName::new(username.into())?;
let password = password.into();
if password.is_empty() {
return Err(Error::Client("Password cannot be empty".to_string()));
}
#[derive(Serialize)]
struct LoginRequest {
username: String,
password: String,
html5: u8,
}
let params = &LoginRequest {
username: username.to_string(),
password,
html5: self.html5,
};
client
.post::<(), LoginRequest>("auth/login", params)
.await?;
Ok(client)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Response<T> {
#[serde(deserialize_with = "number_from_string")]
pub code: u16,
pub status: String,
pub message: String,
#[serde(default = "Option::default")]
pub data: Option<T>,
}
impl<T> Response<T> {
pub(crate) fn into_data(self) -> Result<T> {
self.data.ok_or(Error::Client(
"Expected data in response but got none.".to_string(),
))
}
}
impl Client {
pub async fn login(
base_url: impl AsRef<str>,
username: impl Into<String>,
password: impl Into<String>,
) -> Result<Client> {
ClientBuilder::new(base_url)?
.login(username, password)
.await
}
pub fn builder(base_url: impl AsRef<str>) -> Result<ClientBuilder> {
ClientBuilder::new(base_url)
}
pub async fn logout(&self) -> Result<()> {
self.get::<()>("auth/logout").await?;
Ok(())
}
pub fn system(&self) -> SystemClient {
SystemClient::new(self.clone())
}
pub fn folder(&self, path: impl AsRef<str>) -> Result<FolderClient> {
FolderClient::new(self.clone(), path)
}
pub fn users(&self) -> UsersClient {
UsersClient::new(self.clone())
}
pub fn user(&self, username: impl Into<String>) -> Result<UserClient> {
UserClient::new(self.clone(), username)
}
async fn request<T, B>(
&self,
method: Method,
endpoint: &str,
body: Option<&B>,
) -> Result<Response<T>>
where
T: DeserializeOwned,
B: Serialize,
{
let url = self
.base_url
.join(&format!("api/{}", endpoint))
.map_err(|e| Error::Client(format!("Invalid URL: {e}")))?;
let mut request = self.api.request(method, url);
if let Some(body) = body {
request = request.json(body);
}
let response = request.send().await?;
let status = response.status();
let text = response.text().await?;
if !status.is_success() {
return Err(Error::from_response(status, text));
}
Ok(serde_json::from_str::<Response<T>>(&text).unwrap())
}
pub(crate) async fn get<T>(&self, endpoint: &str) -> Result<Response<T>>
where
T: DeserializeOwned,
{
self.request::<T, ()>(Method::GET, endpoint, None).await
}
pub(crate) async fn post<T, B>(&self, endpoint: &str, body: &B) -> Result<Response<T>>
where
T: DeserializeOwned,
B: Serialize,
{
self.request(Method::POST, endpoint, Some(body)).await
}
pub(crate) async fn put<T, B>(&self, endpoint: &str, body: &B) -> Result<Response<T>>
where
T: DeserializeOwned,
B: Serialize,
{
self.request(Method::PUT, endpoint, Some(body)).await
}
pub(crate) async fn delete<T>(&self, endpoint: &str) -> Result<Response<T>>
where
T: DeserializeOwned,
{
self.request::<T, ()>(Method::DELETE, endpoint, None).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Client, Error, Result};
#[test]
fn valid_client_builder() {
let builder = Client::builder("http://eveng.example.com").unwrap();
assert_eq!(builder.base_url.as_str(), "http://eveng.example.com/");
assert_eq!(builder.timeout, Duration::from_secs(30));
assert_eq!(builder.html5, 1);
assert!(builder.ssl_verify);
}
#[test]
fn invalid_client_builder() {
let err = Client::builder("eveng.example.com").unwrap_err();
assert!(matches!(err, Error::Client(_)));
}
#[test]
fn client_builder_methods() -> Result<()> {
let builder = Client::builder("http://eveng.example.com")?
.timeout(Duration::from_secs(10))
.ssl_verify(false)
.html5(0);
assert_eq!(builder.timeout, Duration::from_secs(10));
assert_eq!(builder.html5, 0);
assert!(!builder.ssl_verify);
Ok(())
}
}