ramp_api/
lib.rs

1//! A fully generated, opinionated API client library for Ramp.
2//!
3//! [![docs.rs](https://docs.rs/ramp-api/badge.svg)](https://docs.rs/ramp-api)
4//!
5//! ## API Details
6//!
7//! Ramp's developer API makes it easier for companies to programmatically set up and manage their card program.
8//!
9//! It provides the ability to automate card issuing and the ability to integrate features like reporting with other software platforms.
10//!
11//! [API Terms of Service](https://ramp.com/developer-agreement)
12//!
13//!
14//!
15//!
16//! ## Client Details
17//!
18//! This client is generated from the [Ramp OpenAPI
19//! specs](https://github.com/sumatokado/ramp-developer) based on API spec version `1.0`. This way it will remain
20//! up to date as features are added. The documentation for the crate is generated
21//! along with the code to make this library easy to use.
22//!
23//!
24//! To install the library, add the following to your `Cargo.toml` file.
25//!
26//! ```toml
27//! [dependencies]
28//! ramp-api = "0.7.0"
29//! ```
30//!
31//! ## Basic example
32//!
33//! Typical use will require intializing a `Client`. This requires
34//! a user agent string and set of credentials.
35//!
36//! ```rust
37//! use ramp_api::Client;
38//!
39//! let ramp = Client::new(
40//!     String::from("client-id"),
41//!     String::from("client-secret"),
42//!     String::from("redirect-uri"),
43//!     String::from("token"),
44//!     String::from("refresh-token")
45//! );
46//! ```
47//!
48//! Alternatively, the library can search for most of the variables required for
49//! the client in the environment:
50//!
51//! - `RAMP_CLIENT_ID`
52//! - `RAMP_CLIENT_SECRET`
53//! - `RAMP_REDIRECT_URI`
54//!
55//! And then you can create a client from the environment.
56//!
57//! ```rust
58//! use ramp_api::Client;
59//!
60//! let ramp = Client::new_from_env(
61//!     String::from("token"),
62//!     String::from("refresh-token")
63//! );
64//! ```
65//!
66//! It is okay to pass empty values for `token` and `refresh_token`. In
67//! the initial state of the client, you will not know these values.
68//!
69//! To start off a fresh client and get a `token` and `refresh_token`, use the following.
70//!
71//! ```rust
72//! use ramp_api::Client;
73//!
74//! async fn do_call() {
75//!     let mut ramp = Client::new_from_env("", "");
76//!
77//!     // Get the URL to request consent from the user.
78//!     // You can optionally pass in scopes. If none are provided, then the
79//!     // resulting URL will not have any scopes.
80//!     let user_consent_url = ramp.user_consent_url(&["some-scope".to_string()]);
81//!
82//!     // In your redirect URL capture the code sent and our state.
83//!     // Send it along to the request for the token.
84//!     let code = "thing-from-redirect-url";
85//!     let state = "state-from-redirect-url";
86//!     let mut access_token = ramp.get_access_token(code, state).await.unwrap();
87//!
88//!     // You can additionally refresh the access token with the following.
89//!     // You must have a refresh token to be able to call this function.
90//!     access_token = ramp.refresh_access_token().await.unwrap();
91//! }
92//! ```
93//!
94#![allow(clippy::derive_partial_eq_without_eq)]
95#![allow(clippy::too_many_arguments)]
96#![allow(clippy::nonstandard_macro_braces)]
97#![allow(clippy::large_enum_variant)]
98#![allow(clippy::tabs_in_doc_comments)]
99#![allow(missing_docs)]
100#![cfg_attr(docsrs, feature(doc_cfg))]
101
102pub mod auths;
103pub mod businesses;
104pub mod card_programs;
105pub mod cards;
106pub mod custom_ids;
107pub mod departments;
108pub mod locations;
109pub mod receipts;
110pub mod reimbursements;
111pub mod transactions;
112pub mod types;
113pub mod users;
114#[doc(hidden)]
115pub mod utils;
116
117pub use reqwest::{header::HeaderMap, StatusCode};
118
119#[derive(Debug)]
120pub struct Response<T> {
121    pub status: reqwest::StatusCode,
122    pub headers: reqwest::header::HeaderMap,
123    pub body: T,
124}
125
126impl<T> Response<T> {
127    pub fn new(status: reqwest::StatusCode, headers: reqwest::header::HeaderMap, body: T) -> Self {
128        Self {
129            status,
130            headers,
131            body,
132        }
133    }
134}
135
136type ClientResult<T> = Result<T, ClientError>;
137
138use thiserror::Error;
139
140/// Errors returned by the client
141#[derive(Debug, Error)]
142pub enum ClientError {
143    // Generic Token Client
144    /// Empty refresh auth token
145    #[error("Refresh AuthToken is empty")]
146    EmptyRefreshToken,
147    /// utf8 convertion error
148    #[error(transparent)]
149    FromUtf8Error(#[from] std::string::FromUtf8Error),
150    /// URL Parsing Error
151    #[error(transparent)]
152    UrlParserError(#[from] url::ParseError),
153    /// Serde JSON parsing error
154    #[error(transparent)]
155    SerdeJsonError(#[from] serde_json::Error),
156    /// Errors returned by reqwest
157    #[error(transparent)]
158    ReqwestError(#[from] reqwest::Error),
159    /// Errors returned by reqwest::header
160    #[error(transparent)]
161    InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
162    /// Errors returned by reqwest middleware
163    #[error(transparent)]
164    ReqwestMiddleWareError(#[from] reqwest_middleware::Error),
165    /// Generic HTTP Error
166    #[error("HTTP Error. Code: {status}, message: {error}")]
167    HttpError {
168        status: http::StatusCode,
169        headers: reqwest::header::HeaderMap,
170        error: String,
171    },
172}
173
174pub const FALLBACK_HOST: &str = "https://api.ramp.com/developer/v1";
175
176mod progenitor_support {
177    use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
178
179    const PATH_SET: &AsciiSet = &CONTROLS
180        .add(b' ')
181        .add(b'"')
182        .add(b'#')
183        .add(b'<')
184        .add(b'>')
185        .add(b'?')
186        .add(b'`')
187        .add(b'{')
188        .add(b'}');
189
190    #[allow(dead_code)]
191    pub(crate) fn encode_path(pc: &str) -> String {
192        utf8_percent_encode(pc, PATH_SET).to_string()
193    }
194}
195
196#[derive(Debug, Default)]
197pub(crate) struct Message {
198    pub body: Option<reqwest::Body>,
199    pub content_type: Option<String>,
200}
201
202use std::convert::TryInto;
203use std::env;
204use std::ops::Add;
205use std::sync::Arc;
206use std::time::{Duration, Instant};
207use tokio::sync::RwLock;
208
209const TOKEN_ENDPOINT: &str = "https://api.ramp.com/v1/public/customer/token";
210const USER_CONSENT_ENDPOINT: &str = "https://app.ramp.com/v1/authorize";
211
212#[derive(Debug, Default, Clone)]
213pub struct RootDefaultServer {}
214
215impl RootDefaultServer {
216    pub fn default_url(&self) -> &str {
217        "https://api.ramp.com/developer/v1"
218    }
219}
220
221/// Entrypoint for interacting with the API client.
222#[derive(Clone)]
223pub struct Client {
224    host: String,
225    host_override: Option<String>,
226    token: Arc<RwLock<InnerToken>>,
227    client_id: String,
228    client_secret: String,
229    redirect_uri: String,
230
231    auto_refresh: bool,
232    client: reqwest_middleware::ClientWithMiddleware,
233}
234
235use schemars::JsonSchema;
236use serde::{Deserialize, Serialize};
237
238#[derive(Debug, JsonSchema, Clone, Default, Serialize, Deserialize)]
239pub struct AccessToken {
240    #[serde(
241        default,
242        skip_serializing_if = "String::is_empty",
243        deserialize_with = "crate::utils::deserialize_null_string::deserialize"
244    )]
245    pub token_type: String,
246
247    #[serde(
248        default,
249        skip_serializing_if = "String::is_empty",
250        deserialize_with = "crate::utils::deserialize_null_string::deserialize"
251    )]
252    pub access_token: String,
253    #[serde(default)]
254    pub expires_in: i64,
255
256    #[serde(
257        default,
258        skip_serializing_if = "String::is_empty",
259        deserialize_with = "crate::utils::deserialize_null_string::deserialize"
260    )]
261    pub refresh_token: String,
262    #[serde(default, alias = "x_refresh_token_expires_in")]
263    pub refresh_token_expires_in: i64,
264
265    #[serde(
266        default,
267        skip_serializing_if = "String::is_empty",
268        deserialize_with = "crate::utils::deserialize_null_string::deserialize"
269    )]
270    pub scope: String,
271}
272
273/// Time in seconds before the access token expiration point that a refresh should
274/// be performed. This value is subtracted from the `expires_in` value returned by
275/// the provider prior to storing
276const REFRESH_THRESHOLD: Duration = Duration::from_secs(60);
277
278#[derive(Debug, Clone)]
279struct InnerToken {
280    access_token: String,
281    refresh_token: String,
282    expires_at: Option<Instant>,
283}
284
285impl Client {
286    /// Create a new Client struct. Requires OAuth2 configuration values as well as an access and refresh token.
287    ///
288    /// # Panics
289    ///
290    /// This function will panic if the internal http client fails to create
291    pub fn new<I, K, R, T, Q>(
292        client_id: I,
293        client_secret: K,
294        redirect_uri: R,
295        token: T,
296        refresh_token: Q,
297    ) -> Self
298    where
299        I: ToString,
300        K: ToString,
301        R: ToString,
302        T: ToString,
303        Q: ToString,
304    {
305        // Retry up to 3 times with increasing intervals between attempts.
306        let retry_policy =
307            reqwest_retry::policies::ExponentialBackoff::builder().build_with_max_retries(3);
308        let client = reqwest::Client::builder()
309            .redirect(reqwest::redirect::Policy::none())
310            .build();
311        match client {
312            Ok(c) => {
313                let client = reqwest_middleware::ClientBuilder::new(c)
314                    // Trace HTTP requests. See the tracing crate to make use of these traces.
315                    .with(reqwest_tracing::TracingMiddleware::default())
316                    // Retry failed requests.
317                    .with(reqwest_conditional_middleware::ConditionalMiddleware::new(
318                        reqwest_retry::RetryTransientMiddleware::new_with_policy(retry_policy),
319                        |req: &reqwest::Request| req.try_clone().is_some(),
320                    ))
321                    .build();
322
323                let host = RootDefaultServer::default().default_url().to_string();
324
325                Client {
326                    host,
327                    host_override: None,
328                    client_id: client_id.to_string(),
329                    client_secret: client_secret.to_string(),
330                    redirect_uri: redirect_uri.to_string(),
331                    token: Arc::new(RwLock::new(InnerToken {
332                        access_token: token.to_string(),
333                        refresh_token: refresh_token.to_string(),
334                        expires_at: None,
335                    })),
336
337                    auto_refresh: false,
338                    client,
339                }
340            }
341            Err(e) => panic!("creating reqwest client failed: {:?}", e),
342        }
343    }
344
345    /// Enables or disables the automatic refreshing of access tokens upon expiration
346    pub fn set_auto_access_token_refresh(&mut self, enabled: bool) -> &mut Self {
347        self.auto_refresh = enabled;
348        self
349    }
350
351    /// Sets a specific `Instant` at which the access token should be considered expired.
352    /// The expiration value will only be used when automatic access token refreshing is
353    /// also enabled. `None` may be passed in if the expiration is unknown. In this case
354    /// automatic refreshes will be attempted when encountering an UNAUTHENTICATED status
355    /// code on a response.
356    pub async fn set_expires_at(&self, expires_at: Option<Instant>) -> &Self {
357        self.token.write().await.expires_at = expires_at;
358        self
359    }
360
361    /// Gets the `Instant` at which the access token used by this client is set to expire
362    /// if one is known
363    pub async fn expires_at(&self) -> Option<Instant> {
364        self.token.read().await.expires_at
365    }
366
367    /// Sets the number of seconds in which the current access token should be considered
368    /// expired
369    pub async fn set_expires_in(&self, expires_in: i64) -> &Self {
370        self.token.write().await.expires_at = Self::compute_expires_at(expires_in);
371        self
372    }
373
374    /// Gets the number of seconds from now in which the current access token will be
375    /// considered expired if one is known
376    pub async fn expires_in(&self) -> Option<Duration> {
377        self.token
378            .read()
379            .await
380            .expires_at
381            .map(|i| i.duration_since(Instant::now()))
382    }
383
384    /// Determines if the access token currently stored in the client is expired. If the
385    /// expiration can not be determined, None is returned
386    pub async fn is_expired(&self) -> Option<bool> {
387        self.token
388            .read()
389            .await
390            .expires_at
391            .map(|expiration| expiration <= Instant::now())
392    }
393
394    fn compute_expires_at(expires_in: i64) -> Option<Instant> {
395        let seconds_valid = expires_in
396            .try_into()
397            .ok()
398            .map(Duration::from_secs)
399            .and_then(|dur| dur.checked_sub(REFRESH_THRESHOLD))
400            .or_else(|| Some(Duration::from_secs(0)));
401
402        seconds_valid.map(|seconds_valid| Instant::now().add(seconds_valid))
403    }
404
405    /// Override the host for all endpoins in the client.
406    pub fn with_host_override<H>(&mut self, host: H) -> &mut Self
407    where
408        H: ToString,
409    {
410        self.host_override = Some(host.to_string());
411        self
412    }
413
414    /// Disables the global host override for the client.
415    pub fn remove_host_override(&mut self) -> &mut Self {
416        self.host_override = None;
417        self
418    }
419
420    pub fn get_host_override(&self) -> Option<&str> {
421        self.host_override.as_deref()
422    }
423
424    pub(crate) fn url(&self, path: &str, host: Option<&str>) -> String {
425        format!(
426            "{}{}",
427            self.get_host_override()
428                .or(host)
429                .unwrap_or(self.host.as_str()),
430            path
431        )
432    }
433
434    /// Create a new Client struct from environment variables. Requires an existing access and refresh token.
435    ///
436    /// The following environment variables are expected to be set:
437    ///   * `RAMP_CLIENT_ID`
438    ///   * `RAMP_CLIENT_SECRET`
439    ///   * `RAMP_REDIRECT_URI`
440    ///
441    /// # Panics
442    ///
443    /// This function will panic if the expected environment variables can not be found
444    pub fn new_from_env<T, R>(token: T, refresh_token: R) -> Self
445    where
446        T: ToString,
447        R: ToString,
448    {
449        let client_id = env::var("RAMP_CLIENT_ID").expect("must set RAMP_CLIENT_ID");
450        let client_secret = env::var("RAMP_CLIENT_SECRET").expect("must set RAMP_CLIENT_SECRET");
451        let redirect_uri = env::var("RAMP_REDIRECT_URI").expect("must set RAMP_REDIRECT_URI");
452
453        Client::new(client_id, client_secret, redirect_uri, token, refresh_token)
454    }
455
456    /// Return a user consent url with an optional set of scopes.
457    /// If no scopes are provided, they will not be passed in the url.
458    pub fn user_consent_url(&self, scopes: &[String]) -> String {
459        let state = uuid::Uuid::new_v4();
460
461        let url = format!(
462            "{}?client_id={}&response_type=code&redirect_uri={}&state={}",
463            USER_CONSENT_ENDPOINT, self.client_id, self.redirect_uri, state
464        );
465
466        if scopes.is_empty() {
467            return url;
468        }
469
470        // Add the scopes.
471        format!("{}&scope={}", url, scopes.join(" "))
472    }
473
474    /// Refresh an access token from a refresh token. Client must have a refresh token
475    /// for this to work.
476    pub async fn refresh_access_token(&self) -> ClientResult<AccessToken> {
477        let response = {
478            let refresh_token = &self.token.read().await.refresh_token;
479
480            if refresh_token.is_empty() {
481                return Err(ClientError::EmptyRefreshToken);
482            }
483
484            let mut headers = reqwest::header::HeaderMap::new();
485            headers.append(
486                reqwest::header::ACCEPT,
487                reqwest::header::HeaderValue::from_static("application/json"),
488            );
489
490            let params = [
491                ("grant_type", "refresh_token"),
492                ("refresh_token", refresh_token),
493                ("client_id", &self.client_id),
494                ("client_secret", &self.client_secret),
495                ("redirect_uri", &self.redirect_uri),
496            ];
497            let client = reqwest::Client::new();
498            client
499                .post(TOKEN_ENDPOINT)
500                .headers(headers)
501                .form(&params)
502                .basic_auth(&self.client_id, Some(&self.client_secret))
503                .send()
504                .await?
505        };
506
507        // Unwrap the response.
508        let t: AccessToken = response.json().await?;
509
510        let refresh_token = self.token.read().await.refresh_token.clone();
511
512        *self.token.write().await = InnerToken {
513            access_token: t.access_token.clone(),
514            refresh_token,
515            expires_at: Self::compute_expires_at(t.expires_in),
516        };
517
518        Ok(t)
519    }
520
521    /// Get an access token from the code returned by the URL paramter sent to the
522    /// redirect URL.
523    pub async fn get_access_token(&mut self, code: &str, state: &str) -> ClientResult<AccessToken> {
524        let mut headers = reqwest::header::HeaderMap::new();
525        headers.append(
526            reqwest::header::ACCEPT,
527            reqwest::header::HeaderValue::from_static("application/json"),
528        );
529
530        let params = [
531            ("grant_type", "authorization_code"),
532            ("code", code),
533            ("client_id", &self.client_id),
534            ("client_secret", &self.client_secret),
535            ("redirect_uri", &self.redirect_uri),
536            ("state", state),
537        ];
538        let client = reqwest::Client::new();
539        let resp = client
540            .post(TOKEN_ENDPOINT)
541            .headers(headers)
542            .form(&params)
543            .basic_auth(&self.client_id, Some(&self.client_secret))
544            .send()
545            .await?;
546
547        // Unwrap the response.
548        let t: AccessToken = resp.json().await?;
549
550        *self.token.write().await = InnerToken {
551            access_token: t.access_token.clone(),
552            refresh_token: t.refresh_token.clone(),
553            expires_at: Self::compute_expires_at(t.expires_in),
554        };
555
556        Ok(t)
557    }
558
559    async fn url_and_auth(&self, uri: &str) -> ClientResult<(reqwest::Url, Option<String>)> {
560        let parsed_url = uri.parse::<reqwest::Url>()?;
561
562        let auth = format!("Bearer {}", self.token.read().await.access_token);
563        Ok((parsed_url, Some(auth)))
564    }
565
566    async fn make_request(
567        &self,
568        method: &reqwest::Method,
569        uri: &str,
570        message: Message,
571    ) -> ClientResult<reqwest::Request> {
572        let (url, auth) = self.url_and_auth(uri).await?;
573
574        let instance = <&Client>::clone(&self);
575
576        let mut req = instance.client.request(method.clone(), url);
577
578        // Set the default headers.
579        req = req.header(
580            reqwest::header::ACCEPT,
581            reqwest::header::HeaderValue::from_static("application/json"),
582        );
583
584        if let Some(content_type) = &message.content_type {
585            req = req.header(
586                reqwest::header::CONTENT_TYPE,
587                reqwest::header::HeaderValue::from_str(content_type).unwrap(),
588            );
589        } else {
590            req = req.header(
591                reqwest::header::CONTENT_TYPE,
592                reqwest::header::HeaderValue::from_static("application/json"),
593            );
594        }
595
596        if let Some(auth_str) = auth {
597            req = req.header(http::header::AUTHORIZATION, &*auth_str);
598        }
599
600        if let Some(body) = message.body {
601            req = req.body(body);
602        }
603
604        Ok(req.build()?)
605    }
606
607    async fn request_raw(
608        &self,
609        method: reqwest::Method,
610        uri: &str,
611        message: Message,
612    ) -> ClientResult<reqwest::Response> {
613        if self.auto_refresh {
614            let expired = self.is_expired().await;
615
616            match expired {
617                // We have a known expired token, we know we need to perform a refresh prior to
618                // attempting to make a request
619                Some(true) => {
620                    self.refresh_access_token().await?;
621                }
622
623                // We have a (theoretically) known good token available. We make an optimistic
624                // attempting at the request. If the token is no longer good, then something other
625                // than the expiration is triggering the failure. We defer handling of these errors
626                // to the caller
627                Some(false) => (),
628
629                // We do not know what state we are in. We could have a valid or expired token.
630                // Generally this means we are in one of two cases:
631                //   1. We have not yet performed a token refresh, nor has the user provided
632                //      expiration data, and therefore do not know the expiration of the user
633                //      provided token
634                //   2. The provider is returning unusable expiration times, at which point we
635                //      choose to ignore them
636                None => (),
637            }
638        }
639
640        let req = self.make_request(&method, uri, message).await?;
641        let resp = self.client.execute(req).await?;
642
643        Ok(resp)
644    }
645
646    async fn request<Out>(
647        &self,
648        method: reqwest::Method,
649        uri: &str,
650        message: Message,
651    ) -> ClientResult<crate::Response<Out>>
652    where
653        Out: serde::de::DeserializeOwned + 'static + Send,
654    {
655        let response = self.request_raw(method, uri, message).await?;
656
657        let status = response.status();
658        let headers = response.headers().clone();
659
660        let response_body = response.bytes().await?;
661
662        if status.is_success() {
663            log::debug!("Received successful response. Read payload.");
664            let parsed_response = if status == http::StatusCode::NO_CONTENT
665                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
666            {
667                serde_json::from_str("null")?
668            } else {
669                serde_json::from_slice::<Out>(&response_body)?
670            };
671            Ok(crate::Response::new(status, headers, parsed_response))
672        } else {
673            let error = if response_body.is_empty() {
674                ClientError::HttpError {
675                    status,
676                    headers,
677                    error: "empty response".into(),
678                }
679            } else {
680                ClientError::HttpError {
681                    status,
682                    headers,
683                    error: String::from_utf8_lossy(&response_body).into(),
684                }
685            };
686
687            Err(error)
688        }
689    }
690
691    async fn request_with_links<Out>(
692        &self,
693        method: http::Method,
694        uri: &str,
695        message: Message,
696    ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Out>)>
697    where
698        Out: serde::de::DeserializeOwned + 'static + Send,
699    {
700        let response = self.request_raw(method, uri, message).await?;
701
702        let status = response.status();
703        let headers = response.headers().clone();
704        let link = response
705            .headers()
706            .get(http::header::LINK)
707            .and_then(|l| l.to_str().ok())
708            .and_then(|l| parse_link_header::parse(l).ok())
709            .as_ref()
710            .and_then(crate::utils::next_link);
711
712        let response_body = response.bytes().await?;
713
714        if status.is_success() {
715            log::debug!("Received successful response. Read payload.");
716
717            let parsed_response = if status == http::StatusCode::NO_CONTENT
718                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
719            {
720                serde_json::from_str("null")?
721            } else {
722                serde_json::from_slice::<Out>(&response_body)?
723            };
724            Ok((link, crate::Response::new(status, headers, parsed_response)))
725        } else {
726            let error = if response_body.is_empty() {
727                ClientError::HttpError {
728                    status,
729                    headers,
730                    error: "empty response".into(),
731                }
732            } else {
733                ClientError::HttpError {
734                    status,
735                    headers,
736                    error: String::from_utf8_lossy(&response_body).into(),
737                }
738            };
739            Err(error)
740        }
741    }
742
743    /* TODO: make this more DRY */
744    #[allow(dead_code)]
745    async fn post_form<Out>(
746        &self,
747        uri: &str,
748        form: reqwest::multipart::Form,
749    ) -> ClientResult<crate::Response<Out>>
750    where
751        Out: serde::de::DeserializeOwned + 'static + Send,
752    {
753        let (url, auth) = self.url_and_auth(uri).await?;
754
755        let instance = <&Client>::clone(&self);
756
757        let mut req = instance.client.request(http::Method::POST, url);
758
759        // Set the default headers.
760        req = req.header(
761            reqwest::header::ACCEPT,
762            reqwest::header::HeaderValue::from_static("application/json"),
763        );
764
765        if let Some(auth_str) = auth {
766            req = req.header(http::header::AUTHORIZATION, &*auth_str);
767        }
768
769        req = req.multipart(form);
770
771        let response = req.send().await?;
772
773        let status = response.status();
774        let headers = response.headers().clone();
775
776        let response_body = response.bytes().await?;
777
778        if status.is_success() {
779            log::debug!("Received successful response. Read payload.");
780            let parsed_response = if status == http::StatusCode::NO_CONTENT
781                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
782            {
783                serde_json::from_str("null")?
784            } else if std::any::TypeId::of::<Out>() == std::any::TypeId::of::<String>() {
785                // Parse the output as a string.
786                let s = String::from_utf8(response_body.to_vec())?;
787                serde_json::from_value(serde_json::json!(&s))?
788            } else {
789                serde_json::from_slice::<Out>(&response_body)?
790            };
791            Ok(crate::Response::new(status, headers, parsed_response))
792        } else {
793            let error = if response_body.is_empty() {
794                ClientError::HttpError {
795                    status,
796                    headers,
797                    error: "empty response".into(),
798                }
799            } else {
800                ClientError::HttpError {
801                    status,
802                    headers,
803                    error: String::from_utf8_lossy(&response_body).into(),
804                }
805            };
806
807            Err(error)
808        }
809    }
810
811    /* TODO: make this more DRY */
812    #[allow(dead_code)]
813    async fn request_with_accept_mime<Out>(
814        &self,
815        method: reqwest::Method,
816        uri: &str,
817        accept_mime_type: &str,
818    ) -> ClientResult<crate::Response<Out>>
819    where
820        Out: serde::de::DeserializeOwned + 'static + Send,
821    {
822        let (url, auth) = self.url_and_auth(uri).await?;
823
824        let instance = <&Client>::clone(&self);
825
826        let mut req = instance.client.request(method, url);
827
828        // Set the default headers.
829        req = req.header(
830            reqwest::header::ACCEPT,
831            reqwest::header::HeaderValue::from_str(accept_mime_type)?,
832        );
833
834        if let Some(auth_str) = auth {
835            req = req.header(http::header::AUTHORIZATION, &*auth_str);
836        }
837
838        let response = req.send().await?;
839
840        let status = response.status();
841        let headers = response.headers().clone();
842
843        let response_body = response.bytes().await?;
844
845        if status.is_success() {
846            log::debug!("Received successful response. Read payload.");
847            let parsed_response = if status == http::StatusCode::NO_CONTENT
848                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
849            {
850                serde_json::from_str("null")?
851            } else if std::any::TypeId::of::<Out>() == std::any::TypeId::of::<String>() {
852                // Parse the output as a string.
853                let s = String::from_utf8(response_body.to_vec())?;
854                serde_json::from_value(serde_json::json!(&s))?
855            } else {
856                serde_json::from_slice::<Out>(&response_body)?
857            };
858            Ok(crate::Response::new(status, headers, parsed_response))
859        } else {
860            let error = if response_body.is_empty() {
861                ClientError::HttpError {
862                    status,
863                    headers,
864                    error: "empty response".into(),
865                }
866            } else {
867                ClientError::HttpError {
868                    status,
869                    headers,
870                    error: String::from_utf8_lossy(&response_body).into(),
871                }
872            };
873
874            Err(error)
875        }
876    }
877
878    /* TODO: make this more DRY */
879    #[allow(dead_code)]
880    async fn request_with_mime<Out>(
881        &self,
882        method: reqwest::Method,
883        uri: &str,
884        content: &[u8],
885        mime_type: &str,
886    ) -> ClientResult<crate::Response<Out>>
887    where
888        Out: serde::de::DeserializeOwned + 'static + Send,
889    {
890        let (url, auth) = self.url_and_auth(uri).await?;
891
892        let instance = <&Client>::clone(&self);
893
894        let mut req = instance.client.request(method, url);
895
896        // Set the default headers.
897        req = req.header(
898            reqwest::header::ACCEPT,
899            reqwest::header::HeaderValue::from_static("application/json"),
900        );
901        req = req.header(
902            reqwest::header::CONTENT_TYPE,
903            reqwest::header::HeaderValue::from_bytes(mime_type.as_bytes()).unwrap(),
904        );
905        // We are likely uploading a file so add the right headers.
906        req = req.header(
907            reqwest::header::HeaderName::from_static("x-upload-content-type"),
908            reqwest::header::HeaderValue::from_static("application/octet-stream"),
909        );
910        req = req.header(
911            reqwest::header::HeaderName::from_static("x-upload-content-length"),
912            reqwest::header::HeaderValue::from_bytes(format!("{}", content.len()).as_bytes())
913                .unwrap(),
914        );
915
916        if let Some(auth_str) = auth {
917            req = req.header(http::header::AUTHORIZATION, &*auth_str);
918        }
919
920        if content.len() > 1 {
921            let b = bytes::Bytes::copy_from_slice(content);
922            // We are uploading a file so add that as the body.
923            req = req.body(b);
924        }
925
926        let response = req.send().await?;
927
928        let status = response.status();
929        let headers = response.headers().clone();
930
931        let response_body = response.bytes().await?;
932
933        if status.is_success() {
934            log::debug!("Received successful response. Read payload.");
935            let parsed_response = if status == http::StatusCode::NO_CONTENT
936                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
937            {
938                serde_json::from_str("null")?
939            } else {
940                serde_json::from_slice::<Out>(&response_body)?
941            };
942            Ok(crate::Response::new(status, headers, parsed_response))
943        } else {
944            let error = if response_body.is_empty() {
945                ClientError::HttpError {
946                    status,
947                    headers,
948                    error: "empty response".into(),
949                }
950            } else {
951                ClientError::HttpError {
952                    status,
953                    headers,
954                    error: String::from_utf8_lossy(&response_body).into(),
955                }
956            };
957
958            Err(error)
959        }
960    }
961
962    async fn request_entity<D>(
963        &self,
964        method: http::Method,
965        uri: &str,
966        message: Message,
967    ) -> ClientResult<crate::Response<D>>
968    where
969        D: serde::de::DeserializeOwned + 'static + Send,
970    {
971        let r = self.request(method, uri, message).await?;
972        Ok(r)
973    }
974
975    #[allow(dead_code)]
976    async fn get<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
977    where
978        D: serde::de::DeserializeOwned + 'static + Send,
979    {
980        self.request_entity(http::Method::GET, uri, message).await
981    }
982
983    #[allow(dead_code)]
984    async fn get_all_pages<D>(&self, uri: &str, _message: Message) -> ClientResult<Response<Vec<D>>>
985    where
986        D: serde::de::DeserializeOwned + 'static + Send,
987    {
988        // TODO: implement this.
989        self.unfold(uri).await
990    }
991
992    /// "unfold" paginated results of a vector of items
993    #[allow(dead_code)]
994    async fn unfold<D>(&self, uri: &str) -> ClientResult<crate::Response<Vec<D>>>
995    where
996        D: serde::de::DeserializeOwned + 'static + Send,
997    {
998        let mut global_items = Vec::new();
999        let (new_link, mut response) = self.get_pages(uri).await?;
1000        let mut link = new_link;
1001        while !response.body.is_empty() {
1002            global_items.append(&mut response.body);
1003            // We need to get the next link.
1004            if let Some(url) = &link {
1005                let url = reqwest::Url::parse(&url.0)?;
1006                let (new_link, new_response) = self.get_pages_url(&url).await?;
1007                link = new_link;
1008                response = new_response;
1009            }
1010        }
1011
1012        Ok(Response::new(
1013            response.status,
1014            response.headers,
1015            global_items,
1016        ))
1017    }
1018
1019    #[allow(dead_code)]
1020    async fn get_pages<D>(
1021        &self,
1022        uri: &str,
1023    ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Vec<D>>)>
1024    where
1025        D: serde::de::DeserializeOwned + 'static + Send,
1026    {
1027        self.request_with_links(http::Method::GET, uri, Message::default())
1028            .await
1029    }
1030
1031    #[allow(dead_code)]
1032    async fn get_pages_url<D>(
1033        &self,
1034        url: &reqwest::Url,
1035    ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Vec<D>>)>
1036    where
1037        D: serde::de::DeserializeOwned + 'static + Send,
1038    {
1039        self.request_with_links(http::Method::GET, url.as_str(), Message::default())
1040            .await
1041    }
1042
1043    #[allow(dead_code)]
1044    async fn post<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
1045    where
1046        D: serde::de::DeserializeOwned + 'static + Send,
1047    {
1048        self.request_entity(http::Method::POST, uri, message).await
1049    }
1050
1051    #[allow(dead_code)]
1052    async fn patch<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
1053    where
1054        D: serde::de::DeserializeOwned + 'static + Send,
1055    {
1056        self.request_entity(http::Method::PATCH, uri, message).await
1057    }
1058
1059    #[allow(dead_code)]
1060    async fn put<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
1061    where
1062        D: serde::de::DeserializeOwned + 'static + Send,
1063    {
1064        self.request_entity(http::Method::PUT, uri, message).await
1065    }
1066
1067    #[allow(dead_code)]
1068    async fn delete<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
1069    where
1070        D: serde::de::DeserializeOwned + 'static + Send,
1071    {
1072        self.request_entity(http::Method::DELETE, uri, message)
1073            .await
1074    }
1075
1076    pub fn auths(&self) -> auths::Auths {
1077        auths::Auths::new(self.clone())
1078    }
1079
1080    pub fn businesses(&self) -> businesses::Businesses {
1081        businesses::Businesses::new(self.clone())
1082    }
1083
1084    pub fn card_programs(&self) -> card_programs::CardPrograms {
1085        card_programs::CardPrograms::new(self.clone())
1086    }
1087
1088    pub fn cards(&self) -> cards::Cards {
1089        cards::Cards::new(self.clone())
1090    }
1091
1092    pub fn custom_ids(&self) -> custom_ids::CustomIds {
1093        custom_ids::CustomIds::new(self.clone())
1094    }
1095
1096    pub fn departments(&self) -> departments::Departments {
1097        departments::Departments::new(self.clone())
1098    }
1099
1100    pub fn locations(&self) -> locations::Locations {
1101        locations::Locations::new(self.clone())
1102    }
1103
1104    pub fn receipts(&self) -> receipts::Receipts {
1105        receipts::Receipts::new(self.clone())
1106    }
1107
1108    pub fn reimbursements(&self) -> reimbursements::Reimbursements {
1109        reimbursements::Reimbursements::new(self.clone())
1110    }
1111
1112    pub fn transactions(&self) -> transactions::Transactions {
1113        transactions::Transactions::new(self.clone())
1114    }
1115
1116    pub fn users(&self) -> users::Users {
1117        users::Users::new(self.clone())
1118    }
1119}