1use std::sync::Arc;
2
3use base64::{engine::general_purpose, Engine as _};
4use http_types::Url;
5use reqwest::header::AUTHORIZATION;
6use reqwest::RequestBuilder;
7use reqwest_middleware;
8use reqwest_retry::policies::ExponentialBackoff;
9use reqwest_retry::RetryTransientMiddleware;
10use serde::{Deserialize, Serialize};
11use tokio::sync::RwLock;
12
13use crate::client::app_info::AppInfo;
14use crate::client::auth::{AuthData, AuthResponse, AuthStrategy, Authenticate};
15use crate::client::endpoint::Endpoint;
16use crate::client::error::{PayPalError, ValidationError};
17use crate::client::request;
18use crate::client::request::QueryParams;
19
20pub static USER_AGENT: &str = concat!("PayPal/v2 Rust Bindings/", env!("CARGO_PKG_VERSION"));
21
22#[derive(Clone)]
23pub struct Client {
24 pub default_headers: request::HttpRequestHeaders,
25 pub auth_data: Arc<RwLock<AuthData>>,
26
27 user_agent: String,
28 client_secret: String,
29 username: String,
30 environment: Environment,
31 base_url: Url,
32 http: reqwest::Client,
33}
34
35impl Client {
36 pub fn new(
42 username: String,
43 client_secret: String,
44 environment: Environment,
45 ) -> Result<Self, Box<PayPalError>> {
46 let authorization =
47 get_basic_auth_for_user_service(username.as_str(), client_secret.as_str());
48
49 let base_url = match environment {
50 Environment::Sandbox => request::RequestUrl::Sandbox,
51 Environment::Live => request::RequestUrl::Live,
52 }
53 .as_url()
54 .map_err(|_e| PayPalError::LibraryError("Could not parse environment Url".to_string()))?;
55
56 Ok(Self {
57 environment,
58 client_secret,
59 username,
60 default_headers: request::HttpRequestHeaders::new(authorization),
61 base_url,
62 http: reqwest::Client::new(),
63 user_agent: USER_AGENT.into(),
64 auth_data: Arc::new(RwLock::new(AuthData::default())),
65 })
66 }
67
68 pub fn compose_url(&self, request_path: &str) -> Url {
73 let mut url = self.base_url.clone();
74 url.set_path(request_path);
75 url
76 }
77
78 pub fn compose_url_with_query(
88 &self,
89 request_path: &str,
90 query: &QueryParams,
91 ) -> Result<Url, serde_qs::Error> {
92 let mut url = self.compose_url(request_path);
93 let params = serde_qs::to_string(query)?;
94
95 if params.is_empty() {
96 return Ok(url);
97 }
98
99 url.set_query(Some(¶ms));
100 Ok(url)
101 }
102
103 #[must_use]
104 pub fn with_app_info(mut self, app_info: &AppInfo) -> Self {
105 self.user_agent = format!("{} {}", self.user_agent, app_info.to_string());
106 self
107 }
108
109 pub async fn get<T: Endpoint>(&self, endpoint: &T) -> Result<T::ResponseBody, PayPalError> {
120 let mut req = self.http.get(endpoint.request_url(self.environment));
121 req = self.set_request_headers(req, &endpoint.headers());
122
123 let response = self.execute(endpoint, req).await?;
124
125 Ok(response)
126 }
127
128 pub async fn post<T: Endpoint>(&self, endpoint: &T) -> Result<T::ResponseBody, PayPalError> {
138 let body = serde_json::to_string(&endpoint.request_body())?;
139 let mut req = self.http.post(endpoint.request_url(self.environment));
140
141 req = self.set_request_headers(req, &endpoint.headers());
142 let response = self.execute(endpoint, req.body(body)).await?;
143
144 Ok(response)
145 }
146
147 pub async fn patch<T: Endpoint>(&self, endpoint: &T) -> Result<(), PayPalError> {
158 let body = serde_json::to_string(&endpoint.request_body())?;
159 let mut req = self.http.patch(endpoint.request_url(self.environment));
160
161 req = self.set_request_headers(req, &endpoint.headers());
162 self.execute(endpoint, req.body(body)).await?;
163
164 Ok(())
165 }
166
167 pub fn set_request_headers(
176 &self,
177 mut request_builder: RequestBuilder,
178 headers: &request::HttpRequestHeaders,
179 ) -> RequestBuilder {
180 for (key, value) in headers.to_vec() {
181 request_builder = request_builder.header(key, value);
182 }
183
184 request_builder
185 }
186
187 async fn execute<T: Endpoint>(
196 &self,
197 endpoint: &T,
198 mut request: RequestBuilder,
199 ) -> Result<T::ResponseBody, PayPalError> {
200 if endpoint.auth_strategy() == AuthStrategy::TokenRefresh
201 && self.auth_data.read().await.about_to_expire()
202 {
203 self.authenticate().await?;
204 }
205
206 request = request.header(
207 AUTHORIZATION,
208 format!("Bearer {}", self.auth_data.read().await.access_token),
209 );
210
211 let response = request.send().await?;
212
213 if !response.status().is_success() {
214 return Err(PayPalError::from(response.json::<ValidationError>().await?));
215 }
216
217 serde_json::from_str::<T::ResponseBody>(&response.text().await?).or_else(|error| {
218 if error.is_eof() {
221 Ok(serde_json::from_str::<T::ResponseBody>("{}")?)
222 } else {
223 Err(error.into())
224 }
225 })
226 }
227
228 pub async fn authenticate(&self) -> Result<(), PayPalError> {
236 let endpoint = Authenticate::new(get_basic_auth_for_user_service(
237 self.username.as_str(),
238 self.client_secret.as_str(),
239 ));
240
241 let mut request = self
242 .http
243 .post(endpoint.request_url(self.environment))
244 .body(serde_urlencoded::to_string(endpoint.request_body())?);
245
246 let mut retries = 0;
247 if let Some(retry_count) = &endpoint.request_strategy().get_retry_count() {
248 retries = (*retry_count).get();
249 }
250
251 request = self.set_request_headers(request, &endpoint.headers());
252 request = request.header(
253 AUTHORIZATION,
254 get_basic_auth_for_user_service(&self.username, &self.client_secret),
255 );
256
257 let retry_client = reqwest_middleware::ClientBuilder::new(self.http.clone())
258 .with(RetryTransientMiddleware::new_with_policy(
259 ExponentialBackoff::builder().build_with_max_retries(retries),
260 ))
261 .build();
262
263 let retry_request = retry_client.execute(request.build()?).await?;
264 let parsed_response = serde_json::from_str::<AuthResponse>(&retry_request.text().await?)?;
265
266 self.auth_data.write().await.update(parsed_response);
267 Ok(())
268 }
269}
270
271fn get_basic_auth_for_user_service(username: &str, client_secret: &str) -> String {
272 format!(
273 "Basic {}",
274 general_purpose::STANDARD.encode(format!("{username}:{client_secret}"))
275 )
276}
277
278#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
279pub enum Environment {
280 Sandbox,
281 Live,
282}
283
284impl Environment {
285 pub const fn as_str(&self) -> &'static str {
286 match self {
287 Self::Sandbox => "sandbox",
288 Self::Live => "live",
289 }
290 }
291}
292
293impl AsRef<str> for Environment {
294 fn as_ref(&self) -> &str {
295 self.as_str()
296 }
297}
298
299impl std::fmt::Display for Environment {
300 fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
301 self.as_str().fmt(formatter)
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use std::str::FromStr;
308
309 use http_types::Url;
310
311 use super::{Client, Environment, QueryParams};
312
313 #[test]
314 fn test_environment() {
315 assert_eq!(Environment::Sandbox.as_str(), "sandbox");
316 assert_eq!(Environment::Live.as_str(), "live");
317 }
318
319 #[test]
320 fn test_compose_url() {
321 let client = Client::new(
322 "username".to_string(),
323 "password".to_string(),
324 Environment::Sandbox,
325 )
326 .unwrap();
327 let url = client.compose_url("test");
328 assert_eq!(
329 url,
330 Url::from_str("https://api-m.sandbox.paypal.com/test").unwrap()
331 );
332
333 let client = Client::new(
334 "username".to_string(),
335 "password".to_string(),
336 Environment::Live,
337 )
338 .unwrap();
339 let url = client.compose_url("test");
340 assert_eq!(url, Url::from_str("https://api-m.paypal.com/test").unwrap());
341 }
342
343 #[test]
344 fn test_compose_url_with_query() {
345 let client = Client::new(
346 "username".to_string(),
347 "password".to_string(),
348 Environment::Sandbox,
349 )
350 .unwrap();
351 let query: QueryParams = QueryParams::new()
352 .page(1)
353 .page_size(10)
354 .total_count_required(true);
355
356 let url = client.compose_url_with_query("test", &query).unwrap();
357
358 assert_eq!(
359 url,
360 Url::from_str("https://api-m.sandbox.paypal.com/test?page=1&page_size=10&total_count_required=true").unwrap()
361 );
362 }
363}