use crate::espocrm_types::Params;
use crate::{debug_if, trace_if};
use hmac::{Hmac, Mac};
use serde::Serialize;
use sha2::Sha256;
use std::fmt::Debug;
use reqwest::{Client, RequestBuilder};
use tap::TapFallible;
type HmacSha256 = Hmac<Sha256>;
pub type NoGeneric = ();
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Method {
Get,
Post,
Put,
Delete,
}
impl From<Method> for reqwest::Method {
fn from(a: Method) -> reqwest::Method {
match a {
Method::Get => reqwest::Method::GET,
Method::Post => reqwest::Method::POST,
Method::Put => reqwest::Method::PUT,
Method::Delete => reqwest::Method::DELETE,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EspoApiClient {
pub(crate) url: String,
pub(crate) username: Option<String>,
pub(crate) password: Option<String>,
pub(crate) api_key: Option<String>,
pub(crate) secret_key: Option<String>,
pub(crate) url_path: String,
}
impl EspoApiClient {
pub fn new(url: &str) -> EspoApiClient {
EspoApiClient {
url: url.to_string(),
username: None,
password: None,
api_key: None,
secret_key: None,
url_path: "/api/v1/".to_string(),
}
}
pub fn build(&self) -> Self {
self.clone()
}
pub fn set_url<S: AsRef<str>>(&mut self, url: S) -> &mut EspoApiClient {
let url = url.as_ref();
let url = if url.ends_with("/") {
let mut url = url.to_string();
url.pop();
url
} else {
url.to_string()
};
self.url = url;
self
}
pub fn set_username<S: AsRef<str>>(&mut self, username: S) -> &mut EspoApiClient {
self.username = Some(username.as_ref().to_string());
self
}
pub fn set_password<S: AsRef<str>>(&mut self, password: S) -> &mut EspoApiClient {
self.password = Some(password.as_ref().to_string());
self
}
pub fn set_api_key<S: AsRef<str>>(&mut self, api_key: S) -> &mut EspoApiClient {
self.api_key = Some(api_key.as_ref().to_string());
self
}
pub fn set_secret_key<S: AsRef<str>>(&mut self, secret_key: S) -> &mut EspoApiClient {
self.secret_key = Some(secret_key.as_ref().to_string());
self
}
pub(crate) fn normalize_url<S: AsRef<str>>(&self, action: S) -> String {
format!("{}{}{}", self.url, self.url_path, action.as_ref())
}
pub async fn create_allow_duplicates<T, S>(&self, action: S, data: T) -> reqwest::Result<reqwest::Response> where T: Serialize + Clone + Debug, S: AsRef<str> {
let url = self.normalize_url(&action);
let client = Client::new();
let mut request = client.post(url);
request = self.configure_client_auth(request, reqwest::Method::POST, action.as_ref());
#[allow(unused)] request
.header("X-Skip-Duplicate-Check", "true")
.json(&data)
.send()
.await
.tap_err(|x| debug_if!("Got an error from EspoCRM: {x}"))
.tap_ok(|x| debug_if!("Got response from EspoCRM with status code: {}", x.status()))
}
pub async fn create<T, S>(&self, action: S, data: T) -> reqwest::Result<reqwest::Response> where T: Serialize + Clone + Debug, S: AsRef<str> {
let url = self.normalize_url(&action.as_ref());
let client = Client::new();
let mut request = client.post(url);
request = self.configure_client_auth(request, reqwest::Method::POST, action.as_ref());
#[allow(unused)] request
.json(&data)
.send()
.await
.tap_err(|x| debug_if!("Got an error from EspoCRM: {x}"))
.tap_ok(|x| debug_if!("Got response from EspoCRM with status code: {}", x.status()))
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(data_get, data_post)))]
pub async fn request<T, S>(
&self,
method: Method,
action: S,
data_get: Option<Params>,
data_post: Option<T>,
) -> reqwest::Result<reqwest::Response>
where
T: Serialize + Clone + Debug,
S: AsRef<str> + Debug,
{
let mut url = self.normalize_url(&action.as_ref());
debug_if!("Using URL {url} to request from EspoCRM");
let reqwest_method = reqwest::Method::from(method);
url = if data_get.is_some() && reqwest_method == reqwest::Method::GET {
format!(
"{}?{}",
url,
crate::serializer::serialize(data_get.unwrap()).unwrap()
)
} else {
url
};
let client = Client::new();
let mut request_builder = client.request(reqwest_method.clone(), url);
request_builder = self.configure_client_auth(request_builder, reqwest_method.clone(), action.as_ref());
if data_post.is_some() {
if reqwest_method != reqwest::Method::GET {
request_builder = request_builder.json(&data_post.clone().unwrap());
request_builder = request_builder.header("Content-Type", "application/json");
}
}
trace_if!("Sending request to EspoCRM");
#[allow(unused)]
request_builder
.send()
.await
.tap_err(|x| debug_if!("Got an error from EspoCRM: {x}"))
.tap_ok(|x| debug_if!("Got response from EspoCRM with status code: {}", x.status()))
}
fn configure_client_auth(&self, mut request_builder: RequestBuilder, request_method: reqwest::Method, action: &str) -> RequestBuilder {
if self.username.is_some() && self.password.is_some() {
trace_if!("Using basic authentication");
request_builder =
request_builder.basic_auth(self.username.clone().unwrap(), self.password.clone());
} else if self.api_key.is_some() && self.secret_key.is_some() {
trace_if!("Using HMAC authentication.");
let str = format!(
"{} /{}",
request_method.clone().to_string(),
action,
);
let mut mac = HmacSha256::new_from_slice(self.secret_key.clone().unwrap().as_bytes())
.expect("Unable to create Hmac instance. Is your key valid?");
mac.update(str.as_bytes());
let mac_result = mac.finalize().into_bytes();
let auth_part = format!(
"{}{}{}",
base64::encode(self.api_key.clone().unwrap().as_bytes()),
"6", base64::encode(mac_result)
);
request_builder = request_builder.header("X-Hmac-Authorization", auth_part);
} else if self.api_key.is_some() {
trace_if!("Authenticating with an API key");
request_builder = request_builder.header("X-Api-Key", self.api_key.clone().unwrap());
}
request_builder
}
}