use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::error::{HevyError, Result};
pub const DEFAULT_BASE_URL: &str = "https://api.hevyapp.com";
#[derive(Debug, Clone)]
pub struct HevyClient {
pub(crate) http: reqwest::Client,
pub(crate) api_key: String,
pub(crate) base_url: String,
}
impl HevyClient {
pub fn new(api_key: impl Into<String>) -> Self {
Self::with_base_url(api_key, DEFAULT_BASE_URL)
}
pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
Self {
http: reqwest::Client::new(),
api_key: api_key.into(),
base_url: base_url.into().trim_end_matches('/').to_string(),
}
}
pub fn with_http_client(
http: reqwest::Client,
api_key: impl Into<String>,
base_url: impl Into<String>,
) -> Self {
Self {
http,
api_key: api_key.into(),
base_url: base_url.into().trim_end_matches('/').to_string(),
}
}
fn url(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
fn authed(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
builder.header("api-key", &self.api_key)
}
pub(crate) async fn get<T: DeserializeOwned>(
&self,
path: &str,
query: &[(&str, Option<String>)],
) -> Result<T> {
let params: Vec<(&str, String)> = query
.iter()
.filter_map(|(k, v)| v.clone().map(|v| (*k, v)))
.collect();
let request = self.authed(self.http.get(self.url(path))).query(¶ms);
let response = request.send().await?;
Self::handle_response(response).await
}
pub(crate) async fn post<B: Serialize + ?Sized, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let request = self.authed(self.http.post(self.url(path))).json(body);
let response = request.send().await?;
Self::handle_response(response).await
}
pub(crate) async fn put<B: Serialize + ?Sized, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let request = self.authed(self.http.put(self.url(path))).json(body);
let response = request.send().await?;
Self::handle_response(response).await
}
pub(crate) async fn post_no_content<B: Serialize + ?Sized>(
&self,
path: &str,
body: &B,
) -> Result<()> {
let request = self.authed(self.http.post(self.url(path))).json(body);
let response = request.send().await?;
Self::handle_empty_response(response).await
}
pub(crate) async fn put_no_content<B: Serialize + ?Sized>(
&self,
path: &str,
body: &B,
) -> Result<()> {
let request = self.authed(self.http.put(self.url(path))).json(body);
let response = request.send().await?;
Self::handle_empty_response(response).await
}
async fn handle_response<T: DeserializeOwned>(response: reqwest::Response) -> Result<T> {
let status = response.status();
let bytes = response.bytes().await?;
if !status.is_success() {
return Err(Self::api_error(status.as_u16(), &bytes));
}
serde_json::from_slice(&bytes).map_err(HevyError::Decode)
}
async fn handle_empty_response(response: reqwest::Response) -> Result<()> {
let status = response.status();
if !status.is_success() {
let bytes = response.bytes().await?;
return Err(Self::api_error(status.as_u16(), &bytes));
}
Ok(())
}
fn api_error(status: u16, bytes: &[u8]) -> HevyError {
let message = serde_json::from_slice::<serde_json::Value>(bytes)
.ok()
.and_then(|v| {
v.get("error")
.and_then(|e| e.as_str())
.map(|s| s.to_string())
})
.unwrap_or_else(|| String::from_utf8_lossy(bytes).trim().to_string());
HevyError::Api { status, message }
}
}