Skip to main content

lib_client_google_docs/
client.rs

1use lib_client_google_auth::AuthStrategy;
2use reqwest::header::HeaderMap;
3use serde::de::DeserializeOwned;
4use std::sync::Arc;
5use tracing::{debug, warn};
6
7use crate::error::{Error, Result};
8use crate::types::*;
9
10const DEFAULT_BASE_URL: &str = "https://docs.googleapis.com/v1";
11
12pub struct ClientBuilder<A> {
13    auth: A,
14    base_url: String,
15}
16
17impl ClientBuilder<()> {
18    pub fn new() -> Self {
19        Self {
20            auth: (),
21            base_url: DEFAULT_BASE_URL.to_string(),
22        }
23    }
24
25    pub fn auth<S: AuthStrategy + 'static>(self, auth: S) -> ClientBuilder<S> {
26        ClientBuilder {
27            auth,
28            base_url: self.base_url,
29        }
30    }
31}
32
33impl Default for ClientBuilder<()> {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl<A: AuthStrategy + 'static> ClientBuilder<A> {
40    pub fn base_url(mut self, url: impl Into<String>) -> Self {
41        self.base_url = url.into();
42        self
43    }
44
45    pub fn build(self) -> Client {
46        Client {
47            http: reqwest::Client::new(),
48            auth: Arc::new(self.auth),
49            base_url: self.base_url,
50        }
51    }
52}
53
54#[derive(Clone)]
55pub struct Client {
56    http: reqwest::Client,
57    auth: Arc<dyn AuthStrategy>,
58    base_url: String,
59}
60
61impl Client {
62    pub fn builder() -> ClientBuilder<()> {
63        ClientBuilder::new()
64    }
65
66    async fn request<T: DeserializeOwned>(
67        &self,
68        method: reqwest::Method,
69        path: &str,
70        body: Option<&impl serde::Serialize>,
71    ) -> Result<T> {
72        let url = format!("{}{}", self.base_url, path);
73        debug!("Docs API request: {} {}", method, url);
74
75        let mut headers = HeaderMap::new();
76        self.auth.apply(&mut headers).await?;
77
78        let mut request = self.http.request(method, &url).headers(headers);
79
80        if let Some(body) = body {
81            request = request.json(body);
82        }
83
84        let response = request.send().await?;
85        self.handle_response(response).await
86    }
87
88    async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
89        let status = response.status();
90
91        if status.is_success() {
92            let body = response.text().await?;
93            serde_json::from_str(&body).map_err(Error::from)
94        } else {
95            let status_code = status.as_u16();
96            let body = response.text().await.unwrap_or_default();
97            warn!("Docs API error ({}): {}", status_code, body);
98
99            match status_code {
100                401 => Err(Error::Unauthorized),
101                404 => Err(Error::NotFound(body)),
102                429 => Err(Error::RateLimited { retry_after: 60 }),
103                _ => Err(Error::Api {
104                    status: status_code,
105                    message: body,
106                }),
107            }
108        }
109    }
110
111    /// Get a document.
112    pub async fn get_document(&self, id: &str) -> Result<Document> {
113        self.request(
114            reqwest::Method::GET,
115            &format!("/documents/{}", id),
116            None::<&()>,
117        )
118        .await
119    }
120
121    /// Create a document.
122    pub async fn create_document(&self, request: CreateDocumentRequest) -> Result<Document> {
123        self.request(reqwest::Method::POST, "/documents", Some(&request))
124            .await
125    }
126
127    /// Batch update a document.
128    pub async fn batch_update(
129        &self,
130        id: &str,
131        request: BatchUpdateRequest,
132    ) -> Result<BatchUpdateResponse> {
133        self.request(
134            reqwest::Method::POST,
135            &format!("/documents/{}:batchUpdate", id),
136            Some(&request),
137        )
138        .await
139    }
140
141    /// Insert text at a specific index.
142    pub async fn insert_text(&self, id: &str, index: i32, text: &str) -> Result<BatchUpdateResponse> {
143        let request = BatchUpdateRequest {
144            requests: vec![Request::insert_text(text, index)],
145        };
146        self.batch_update(id, request).await
147    }
148
149    /// Delete content in a range.
150    pub async fn delete_content(&self, id: &str, start: i32, end: i32) -> Result<BatchUpdateResponse> {
151        let request = BatchUpdateRequest {
152            requests: vec![Request::delete_range(start, end)],
153        };
154        self.batch_update(id, request).await
155    }
156
157    /// Get document text content.
158    pub async fn get_text(&self, id: &str) -> Result<String> {
159        let doc = self.get_document(id).await?;
160        Ok(extract_text(&doc))
161    }
162}
163
164/// Extract plain text from a document.
165fn extract_text(doc: &Document) -> String {
166    let mut text = String::new();
167    if let Some(body) = &doc.body {
168        if let Some(content) = &body.content {
169            for element in content {
170                if let Some(paragraph) = &element.paragraph {
171                    for elem in &paragraph.elements {
172                        if let Some(text_run) = &elem.text_run {
173                            text.push_str(&text_run.content);
174                        }
175                    }
176                }
177            }
178        }
179    }
180    text
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use lib_client_google_auth::ApiKeyAuth;
187
188    #[test]
189    fn test_builder() {
190        let client = Client::builder()
191            .auth(ApiKeyAuth::new("test-key"))
192            .build();
193        assert_eq!(client.base_url, DEFAULT_BASE_URL);
194    }
195}