tftio-org-gdocs 0.1.3

Sync org-mode documents to Google Docs and pull reviewer comments back into org-mode
Documentation
//! The Google Docs and Drive API hubs, sharing one authenticator.
//!
//! Both hubs are generated by `google-docs1` / `google-drive3` over a shared
//! `hyper` + `hyper-rustls` client. This wrapper centralizes construction and the
//! `root_url` override that lets mock-server tests retarget the hubs (OVR-3).

use google_docs1::Docs;
use google_docs1::common::{Error as ApiError, GetToken};
use google_drive3::DriveHub;
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;

use crate::auth::{Connector, https_connector};
use crate::error::{Error, Result};

/// Bundled Google Docs and Drive hubs backed by one authenticator.
pub struct GoogleClient {
    /// Google Docs API hub (`documents.create` / `get` / `batchUpdate`).
    pub docs: Docs<Connector>,
    /// Google Drive API hub (`comments.list` / `update`).
    pub drive: DriveHub<Connector>,
}

impl GoogleClient {
    /// Build both hubs from an authenticator, targeting the default Google roots.
    ///
    /// Generic over the authenticator so production passes the
    /// [`crate::auth::GoogleAuthenticator`] while tests can pass any
    /// [`GetToken`] (e.g. a `String` static token).
    ///
    /// # Errors
    ///
    /// Returns [`crate::error::Error::Auth`] when the HTTPS connector cannot be
    /// constructed.
    pub fn new<A: GetToken + Clone + 'static>(auth: A) -> Result<Self> {
        let docs_client = Client::builder(TokioExecutor::new()).build(https_connector()?);
        let drive_client = Client::builder(TokioExecutor::new()).build(https_connector()?);
        let docs = Docs::new(docs_client, auth.clone());
        let drive = DriveHub::new(drive_client, auth);
        Ok(Self { docs, drive })
    }

    /// Point both hubs at `base_url` instead of the live Google endpoints.
    ///
    /// The generated call builders construct request URLs from each hub's
    /// `_base_url`, so the override must target that field (not `root_url`). A
    /// trailing `/` is ensured because the builders append the path directly.
    /// Used by `mockito` tests to assert request shapes without network access
    /// (OVR-3 CI tier).
    pub fn set_base_url(&mut self, base_url: &str) {
        let normalized = if base_url.ends_with('/') {
            base_url.to_owned()
        } else {
            format!("{base_url}/")
        };
        let _ = self.docs.base_url(normalized.clone());
        let _ = self.docs.root_url(normalized.clone());
        let _ = self.drive.base_url(normalized.clone());
        let _ = self.drive.root_url(normalized);
    }
}

/// Map the generated Google client's error into the crate's typed error.
///
/// This is the single seam (F6) where the foreign error type is consumed; no
/// library code above the [`crate::google`] module sees it (EI-1). The error is
/// reduced to its display string so the foreign type never enters the public
/// surface.
#[allow(
    clippy::needless_pass_by_value,
    reason = "used directly as a `map_err` adapter, which passes the error by value"
)]
pub(crate) fn map_api_error(error: ApiError) -> Error {
    Error::Google(error.to_string())
}