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}