Skip to main content

org_gdocs/google/
client.rs

1//! The Google Docs and Drive API hubs, sharing one authenticator.
2//!
3//! Both hubs are generated by `google-docs1` / `google-drive3` over a shared
4//! `hyper` + `hyper-rustls` client. This wrapper centralizes construction and the
5//! `root_url` override that lets mock-server tests retarget the hubs (OVR-3).
6
7use google_docs1::Docs;
8use google_docs1::common::{Error as ApiError, GetToken};
9use google_drive3::DriveHub;
10use hyper_util::client::legacy::Client;
11use hyper_util::rt::TokioExecutor;
12
13use crate::auth::{Connector, https_connector};
14use crate::error::{Error, Result};
15
16/// Bundled Google Docs and Drive hubs backed by one authenticator.
17pub struct GoogleClient {
18    /// Google Docs API hub (`documents.create` / `get` / `batchUpdate`).
19    pub docs: Docs<Connector>,
20    /// Google Drive API hub (`comments.list` / `update`).
21    pub drive: DriveHub<Connector>,
22}
23
24impl GoogleClient {
25    /// Build both hubs from an authenticator, targeting the default Google roots.
26    ///
27    /// Generic over the authenticator so production passes the
28    /// [`crate::auth::GoogleAuthenticator`] while tests can pass any
29    /// [`GetToken`] (e.g. a `String` static token).
30    ///
31    /// # Errors
32    ///
33    /// Returns [`crate::error::Error::Auth`] when the HTTPS connector cannot be
34    /// constructed.
35    pub fn new<A: GetToken + Clone + 'static>(auth: A) -> Result<Self> {
36        let docs_client = Client::builder(TokioExecutor::new()).build(https_connector()?);
37        let drive_client = Client::builder(TokioExecutor::new()).build(https_connector()?);
38        let docs = Docs::new(docs_client, auth.clone());
39        let drive = DriveHub::new(drive_client, auth);
40        Ok(Self { docs, drive })
41    }
42
43    /// Point both hubs at `base_url` instead of the live Google endpoints.
44    ///
45    /// The generated call builders construct request URLs from each hub's
46    /// `_base_url`, so the override must target that field (not `root_url`). A
47    /// trailing `/` is ensured because the builders append the path directly.
48    /// Used by `mockito` tests to assert request shapes without network access
49    /// (OVR-3 CI tier).
50    pub fn set_base_url(&mut self, base_url: &str) {
51        let normalized = if base_url.ends_with('/') {
52            base_url.to_owned()
53        } else {
54            format!("{base_url}/")
55        };
56        let _ = self.docs.base_url(normalized.clone());
57        let _ = self.docs.root_url(normalized.clone());
58        let _ = self.drive.base_url(normalized.clone());
59        let _ = self.drive.root_url(normalized);
60    }
61}
62
63/// Map the generated Google client's error into the crate's typed error.
64///
65/// This is the single seam (F6) where the foreign error type is consumed; no
66/// library code above the [`crate::google`] module sees it (EI-1). The error is
67/// reduced to its display string so the foreign type never enters the public
68/// surface.
69#[allow(
70    clippy::needless_pass_by_value,
71    reason = "used directly as a `map_err` adapter, which passes the error by value"
72)]
73pub(crate) fn map_api_error(error: ApiError) -> Error {
74    Error::Google(error.to_string())
75}