lib_client_google_docs/
client.rs1use 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 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 pub async fn create_document(&self, request: CreateDocumentRequest) -> Result<Document> {
123 self.request(reqwest::Method::POST, "/documents", Some(&request))
124 .await
125 }
126
127 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 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 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 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
164fn 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 ¶graph.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}