google_groups_settings/
lib.rs

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