1use mkt_core::error::{MktError, Result};
9use mkt_core::http::{OpKind, RateLimiter, RetryPolicy, retry, retry_after_secs};
10use reqwest::Client;
11use secrecy::{ExposeSecret, SecretString};
12use tracing::instrument;
13
14use crate::error::GoogleApiErrorResponse;
15
16const API_VERSION: &str = "v24";
18
19const REQUESTS_PER_SECOND: u32 = 5;
23
24#[derive(Debug)]
26pub struct GoogleClient {
27 http: Client,
28 base_url: String,
29 access_token: SecretString,
30 developer_token: String,
31 customer_id: String,
32 login_customer_id: Option<String>,
33 rate_limiter: RateLimiter,
34 retry: RetryPolicy,
35}
36
37impl GoogleClient {
38 pub fn new(
47 access_token: SecretString,
48 developer_token: String,
49 customer_id: &str,
50 login_customer_id: Option<&str>,
51 ) -> Result<Self> {
52 let base_url = format!("https://googleads.googleapis.com/{API_VERSION}/");
53 Ok(Self::new_with_base_url(
54 access_token,
55 developer_token,
56 customer_id,
57 login_customer_id,
58 base_url,
59 )?
60 .with_retry_policy(RetryPolicy::standard()))
61 }
62
63 pub fn new_with_base_url(
71 access_token: SecretString,
72 developer_token: String,
73 customer_id: &str,
74 login_customer_id: Option<&str>,
75 base_url: String,
76 ) -> Result<Self> {
77 let http = mkt_core::http::build_http_client(None)?;
78 Ok(Self {
79 http,
80 base_url,
81 access_token,
82 developer_token,
83 customer_id: customer_id.replace('-', ""),
84 login_customer_id: login_customer_id.map(|id| id.replace('-', "")),
85 rate_limiter: RateLimiter::new(REQUESTS_PER_SECOND),
86 retry: RetryPolicy::none(),
87 })
88 }
89
90 #[must_use]
93 pub const fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
94 self.retry = policy;
95 self
96 }
97
98 pub fn customer_id(&self) -> &str {
100 &self.customer_id
101 }
102
103 #[instrument(skip(self, query), fields(provider = "google"))]
110 pub async fn search(&self, query: &str) -> Result<serde_json::Value> {
111 self.search_page(query, None).await
112 }
113
114 #[instrument(skip(self, query, page_token), fields(provider = "google"))]
121 pub async fn search_page(
122 &self,
123 query: &str,
124 page_token: Option<&str>,
125 ) -> Result<serde_json::Value> {
126 let path = format!("customers/{}/googleAds:search", self.customer_id);
127 let mut body = serde_json::json!({ "query": query });
128 if let Some(token) = page_token {
129 body["pageToken"] = serde_json::Value::String(token.to_string());
130 }
131 self.post(&path, &body, OpKind::Read).await
132 }
133
134 #[instrument(skip(self, operations), fields(provider = "google"))]
142 pub async fn mutate(
143 &self,
144 resource: &str,
145 operations: &serde_json::Value,
146 ) -> Result<serde_json::Value> {
147 let path = format!("customers/{}/{resource}:mutate", self.customer_id);
148 let body = serde_json::json!({ "operations": operations });
149 self.post(&path, &body, OpKind::Write).await
150 }
151
152 #[instrument(skip(self, operations), fields(provider = "google"))]
168 pub async fn mutate_atomic(&self, operations: &serde_json::Value) -> Result<serde_json::Value> {
169 let path = format!("customers/{}/googleAds:mutate", self.customer_id);
170 let body = serde_json::json!({
171 "mutateOperations": operations,
172 "partialFailure": false,
173 "responseContentType": "MUTABLE_RESOURCE",
174 });
175 self.post(&path, &body, OpKind::Write).await
176 }
177
178 async fn post(
180 &self,
181 path: &str,
182 body: &serde_json::Value,
183 kind: OpKind,
184 ) -> Result<serde_json::Value> {
185 self.rate_limiter.acquire(1).await?;
186 let url = format!("{}{path}", self.base_url);
187
188 retry(&self.retry, kind, || async {
189 let mut request = self
190 .http
191 .post(&url)
192 .bearer_auth(self.access_token.expose_secret())
193 .header("developer-token", &self.developer_token)
194 .json(body);
195
196 if let Some(login_id) = &self.login_customer_id {
197 request = request.header("login-customer-id", login_id);
198 }
199
200 let response = request.send().await?;
201 Self::parse_response(response).await
202 })
203 .await
204 }
205
206 async fn parse_response(response: reqwest::Response) -> Result<serde_json::Value> {
208 let status = response.status().as_u16();
209 let retry_after = retry_after_secs(response.headers());
210 let body = response.text().await?;
211
212 if (200..300).contains(&status) {
213 let value: serde_json::Value = serde_json::from_str(&body)?;
214 return Ok(value);
215 }
216
217 if status == 429 {
218 return Err(MktError::RateLimited {
219 provider: "google".into(),
220 retry_after_secs: retry_after.unwrap_or(60),
221 });
222 }
223
224 if let Ok(api_err) = serde_json::from_str::<GoogleApiErrorResponse>(&body) {
225 return Err(api_err.into_mkt_error(status));
226 }
227
228 Err(MktError::ApiError {
229 provider: "google".into(),
230 status,
231 message: body,
232 retry_after,
233 })
234 }
235}