use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::content::Content;
use crate::error::{ErrorResponse, NotraError, Result};
const DEFAULT_BASE_URL: &str = "https://api.usenotra.com";
const DEFAULT_TIMEOUT_SECS: u64 = 30;
pub struct Notra {
pub(crate) http: reqwest::Client,
pub(crate) base_url: String,
}
impl Notra {
pub fn builder() -> NotraBuilder {
NotraBuilder::default()
}
pub fn content(&self) -> Content<'_> {
Content::new(self)
}
pub(crate) async fn get<T: DeserializeOwned>(
&self,
path: &str,
query: &[(&str, String)],
) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let resp = self.http.get(&url).query(query).send().await?;
self.handle_response(resp).await
}
pub(crate) async fn post<T: DeserializeOwned, B: Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let resp = self.http.post(&url).json(body).send().await?;
self.handle_response(resp).await
}
pub(crate) async fn patch<T: DeserializeOwned, B: Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let resp = self.http.patch(&url).json(body).send().await?;
self.handle_response(resp).await
}
pub(crate) async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let resp = self.http.delete(&url).send().await?;
self.handle_response(resp).await
}
async fn handle_response<T: DeserializeOwned>(
&self,
resp: reqwest::Response,
) -> Result<T> {
let status = resp.status();
if status.is_success() {
Ok(resp.json().await?)
} else {
let body = resp.text().await.unwrap_or_default();
let message = serde_json::from_str::<ErrorResponse>(&body)
.ok()
.and_then(|e| e.message)
.unwrap_or(body);
Err(NotraError::Api {
status: status.as_u16(),
message,
})
}
}
}
#[derive(Default)]
pub struct NotraBuilder {
bearer_auth: Option<String>,
server_url: Option<String>,
timeout: Option<Duration>,
}
impl NotraBuilder {
pub fn bearer_auth(mut self, token: impl Into<String>) -> Self {
self.bearer_auth = Some(token.into());
self
}
pub fn server_url(mut self, url: impl Into<String>) -> Self {
self.server_url = Some(url.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn build(self) -> Result<Notra> {
let token = self
.bearer_auth
.ok_or_else(|| NotraError::Builder("bearer_auth is required".into()))?;
let mut headers = HeaderMap::new();
let auth_value = format!("Bearer {}", token);
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&auth_value)
.map_err(|e| NotraError::Builder(format!("invalid auth token: {e}")))?,
);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let timeout = self.timeout.unwrap_or(Duration::from_secs(DEFAULT_TIMEOUT_SECS));
let http = reqwest::Client::builder()
.default_headers(headers)
.timeout(timeout)
.build()
.map_err(|e| NotraError::Builder(format!("failed to build HTTP client: {e}")))?;
let base_url = self
.server_url
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
Ok(Notra { http, base_url })
}
}