espocrm_rs/
espocrm_api_client.rs1use crate::espocrm_types::Params;
2use crate::{debug_if, trace_if};
3use hmac::{Hmac, Mac};
4use serde::Serialize;
5use sha2::Sha256;
6use std::fmt::Debug;
7use reqwest::{Client, RequestBuilder};
8use tap::TapFallible;
9
10type HmacSha256 = Hmac<Sha256>;
11
12pub type NoGeneric = ();
15
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub enum Method {
18 Get,
19 Post,
20 Put,
21 Delete,
22}
23
24impl From<Method> for reqwest::Method {
25 fn from(a: Method) -> reqwest::Method {
26 match a {
27 Method::Get => reqwest::Method::GET,
28 Method::Post => reqwest::Method::POST,
29 Method::Put => reqwest::Method::PUT,
30 Method::Delete => reqwest::Method::DELETE,
31 }
32 }
33}
34
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct EspoApiClient {
37 pub(crate) url: String,
38 pub(crate) username: Option<String>,
39 pub(crate) password: Option<String>,
40 pub(crate) api_key: Option<String>,
41 pub(crate) secret_key: Option<String>,
42 pub(crate) url_path: String,
43}
44
45impl EspoApiClient {
46 pub fn new(url: &str) -> EspoApiClient {
48 EspoApiClient {
49 url: url.to_string(),
50 username: None,
51 password: None,
52 api_key: None,
53 secret_key: None,
54 url_path: "/api/v1/".to_string(),
55 }
56 }
57
58 pub fn build(&self) -> Self {
59 self.clone()
60 }
61
62 pub fn set_url<S: AsRef<str>>(&mut self, url: S) -> &mut EspoApiClient {
64 let url = url.as_ref();
65
66 let url = if url.ends_with("/") {
67 let mut url = url.to_string();
68 url.pop();
69 url
70 } else {
71 url.to_string()
72 };
73
74 self.url = url;
75 self
76 }
77
78 pub fn set_username<S: AsRef<str>>(&mut self, username: S) -> &mut EspoApiClient {
82 self.username = Some(username.as_ref().to_string());
83 self
84 }
85
86 pub fn set_password<S: AsRef<str>>(&mut self, password: S) -> &mut EspoApiClient {
90 self.password = Some(password.as_ref().to_string());
91 self
92 }
93
94 pub fn set_api_key<S: AsRef<str>>(&mut self, api_key: S) -> &mut EspoApiClient {
98 self.api_key = Some(api_key.as_ref().to_string());
99 self
100 }
101
102 pub fn set_secret_key<S: AsRef<str>>(&mut self, secret_key: S) -> &mut EspoApiClient {
105 self.secret_key = Some(secret_key.as_ref().to_string());
106 self
107 }
108
109 pub(crate) fn normalize_url<S: AsRef<str>>(&self, action: S) -> String {
110 format!("{}{}{}", self.url, self.url_path, action.as_ref())
111 }
112
113 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> {
122 let url = self.normalize_url(&action);
123 let client = Client::new();
124 let mut request = client.post(url);
125 request = self.configure_client_auth(request, reqwest::Method::POST, action.as_ref());
126
127 #[allow(unused)] request
129 .header("X-Skip-Duplicate-Check", "true")
130 .json(&data)
131 .send()
132 .await
133 .tap_err(|x| debug_if!("Got an error from EspoCRM: {x}"))
134 .tap_ok(|x| debug_if!("Got response from EspoCRM with status code: {}", x.status()))
135
136 }
137
138 pub async fn create<T, S>(&self, action: S, data: T) -> reqwest::Result<reqwest::Response> where T: Serialize + Clone + Debug, S: AsRef<str> {
147 let url = self.normalize_url(&action.as_ref());
148 let client = Client::new();
149 let mut request = client.post(url);
150 request = self.configure_client_auth(request, reqwest::Method::POST, action.as_ref());
151
152 #[allow(unused)] request
154 .json(&data)
155 .send()
156 .await
157 .tap_err(|x| debug_if!("Got an error from EspoCRM: {x}"))
158 .tap_ok(|x| debug_if!("Got response from EspoCRM with status code: {}", x.status()))
159 }
160
161 #[cfg_attr(feature = "tracing", tracing::instrument(skip(data_get, data_post)))]
171 pub async fn request<T, S>(
172 &self,
173 method: Method,
174 action: S,
175 data_get: Option<Params>,
176 data_post: Option<T>,
177 ) -> reqwest::Result<reqwest::Response>
178 where
179 T: Serialize + Clone + Debug,
180 S: AsRef<str> + Debug,
181 {
182 let mut url = self.normalize_url(&action.as_ref());
183 debug_if!("Using URL {url} to request from EspoCRM");
184
185 let reqwest_method = reqwest::Method::from(method);
186
187 url = if data_get.is_some() && reqwest_method == reqwest::Method::GET {
188 format!(
189 "{}?{}",
190 url,
191 crate::serializer::serialize(data_get.unwrap()).unwrap()
192 )
193 } else {
194 url
195 };
196
197 let client = Client::new();
198 let mut request_builder = client.request(reqwest_method.clone(), url);
199 request_builder = self.configure_client_auth(request_builder, reqwest_method.clone(), action.as_ref());
200
201 if data_post.is_some() {
202 if reqwest_method != reqwest::Method::GET {
203 request_builder = request_builder.json(&data_post.clone().unwrap());
204 request_builder = request_builder.header("Content-Type", "application/json");
205 }
206 }
207
208 trace_if!("Sending request to EspoCRM");
209 #[allow(unused)]
210 request_builder
211 .send()
212 .await
213 .tap_err(|x| debug_if!("Got an error from EspoCRM: {x}"))
214 .tap_ok(|x| debug_if!("Got response from EspoCRM with status code: {}", x.status()))
215 }
216
217 fn configure_client_auth(&self, mut request_builder: RequestBuilder, request_method: reqwest::Method, action: &str) -> RequestBuilder {
218 if self.username.is_some() && self.password.is_some() {
220 trace_if!("Using basic authentication");
221 request_builder =
222 request_builder.basic_auth(self.username.clone().unwrap(), self.password.clone());
223
224 } else if self.api_key.is_some() && self.secret_key.is_some() {
226 trace_if!("Using HMAC authentication.");
227
228 let str = format!(
229 "{} /{}",
230 request_method.clone().to_string(),
231 action,
232 );
233
234 let mut mac = HmacSha256::new_from_slice(self.secret_key.clone().unwrap().as_bytes())
235 .expect("Unable to create Hmac instance. Is your key valid?");
236 mac.update(str.as_bytes());
237 let mac_result = mac.finalize().into_bytes();
238
239 let auth_part = format!(
240 "{}{}{}",
241 base64::encode(self.api_key.clone().unwrap().as_bytes()),
242 "6", base64::encode(mac_result)
244 );
245
246 request_builder = request_builder.header("X-Hmac-Authorization", auth_part);
247
248 } else if self.api_key.is_some() {
250 trace_if!("Authenticating with an API key");
251
252 request_builder = request_builder.header("X-Api-Key", self.api_key.clone().unwrap());
253 }
254
255 request_builder
256 }
257}