1use mkt_core::error::{MktError, Result};
9use mkt_core::http::RateLimiter;
10use reqwest::Client;
11use secrecy::{ExposeSecret, SecretString};
12use tracing::instrument;
13
14use crate::error::GoogleApiErrorResponse;
15
16const API_VERSION: &str = "v24";
18
19const MAX_CONCURRENT: usize = 100;
21
22#[derive(Debug)]
24pub struct GoogleClient {
25 http: Client,
26 base_url: String,
27 access_token: SecretString,
28 developer_token: String,
29 customer_id: String,
30 login_customer_id: Option<String>,
31 rate_limiter: RateLimiter,
32}
33
34impl GoogleClient {
35 pub fn new(
44 access_token: SecretString,
45 developer_token: String,
46 customer_id: &str,
47 login_customer_id: Option<&str>,
48 ) -> Result<Self> {
49 let base_url = format!("https://googleads.googleapis.com/{API_VERSION}/");
50 Self::new_with_base_url(
51 access_token,
52 developer_token,
53 customer_id,
54 login_customer_id,
55 base_url,
56 )
57 }
58
59 pub fn new_with_base_url(
65 access_token: SecretString,
66 developer_token: String,
67 customer_id: &str,
68 login_customer_id: Option<&str>,
69 base_url: String,
70 ) -> Result<Self> {
71 let http = mkt_core::http::build_http_client(None)?;
72 Ok(Self {
73 http,
74 base_url,
75 access_token,
76 developer_token,
77 customer_id: customer_id.replace('-', ""),
78 login_customer_id: login_customer_id.map(|id| id.replace('-', "")),
79 rate_limiter: RateLimiter::new(MAX_CONCURRENT),
80 })
81 }
82
83 pub fn customer_id(&self) -> &str {
85 &self.customer_id
86 }
87
88 #[instrument(skip(self, query), fields(provider = "google"))]
95 pub async fn search(&self, query: &str) -> Result<serde_json::Value> {
96 let path = format!("customers/{}/googleAds:search", self.customer_id);
97 let body = serde_json::json!({ "query": query });
98 self.post(&path, &body).await
99 }
100
101 #[instrument(skip(self, operations), fields(provider = "google"))]
109 pub async fn mutate(
110 &self,
111 resource: &str,
112 operations: &serde_json::Value,
113 ) -> Result<serde_json::Value> {
114 let path = format!("customers/{}/{resource}:mutate", self.customer_id);
115 let body = serde_json::json!({ "operations": operations });
116 self.post(&path, &body).await
117 }
118
119 async fn post(&self, path: &str, body: &serde_json::Value) -> Result<serde_json::Value> {
121 self.rate_limiter.acquire(1).await?;
122 let url = format!("{}{path}", self.base_url);
123
124 let mut request = self
125 .http
126 .post(&url)
127 .bearer_auth(self.access_token.expose_secret())
128 .header("developer-token", &self.developer_token)
129 .json(body);
130
131 if let Some(login_id) = &self.login_customer_id {
132 request = request.header("login-customer-id", login_id);
133 }
134
135 let response = request.send().await?;
136 Self::parse_response(response).await
137 }
138
139 async fn parse_response(response: reqwest::Response) -> Result<serde_json::Value> {
141 let status = response.status().as_u16();
142 let body = response.text().await?;
143
144 if (200..300).contains(&status) {
145 let value: serde_json::Value = serde_json::from_str(&body)?;
146 return Ok(value);
147 }
148
149 if let Ok(api_err) = serde_json::from_str::<GoogleApiErrorResponse>(&body) {
150 return Err(api_err.into_mkt_error(status));
151 }
152
153 Err(MktError::ApiError {
154 provider: "google".into(),
155 status,
156 message: body,
157 retry_after: None,
158 })
159 }
160}