1use std::sync::Mutex;
2use std::{fs, path::PathBuf};
3
4use reqwest::{Client as HttpClient, Identity, Method, StatusCode};
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7
8use crate::auth::AccessToken;
9use crate::environment::{Endpoints, Environment};
10use crate::error::Error;
11
12pub struct Client {
13 pub(crate) id: String,
14 pub(crate) secret: String,
15 pub(crate) environment: Environment,
16 pub(crate) http: HttpClient,
17 pub(crate) token: Mutex<Option<AccessToken>>,
18}
19
20enum MtlsSource {
21 Pkcs12Der { der: Vec<u8>, password: String },
22 Pkcs12File { path: PathBuf, password: String },
23}
24
25pub struct ClientBuilder {
26 id: Option<String>,
27 secret: Option<String>,
28 environment: Environment,
29 http: Option<HttpClient>,
30 mtls_source: Option<MtlsSource>,
31}
32
33impl Default for ClientBuilder {
34 fn default() -> Self {
35 Self {
36 id: None,
37 secret: None,
38 environment: Environment::Sandbox,
39 http: None,
40 mtls_source: None,
41 }
42 }
43}
44
45impl ClientBuilder {
46 #[must_use]
47 pub fn new() -> Self {
48 Self::default()
49 }
50 #[must_use]
51 pub fn client_id(mut self, value: impl Into<String>) -> Self {
52 self.id = Some(value.into());
53 self
54 }
55
56 #[must_use]
57 pub fn client_secret(mut self, value: impl Into<String>) -> Self {
58 self.secret = Some(value.into());
59 self
60 }
61
62 #[must_use]
63 pub fn credentials(
64 mut self,
65 client_id: impl Into<String>,
66 client_secret: impl Into<String>,
67 ) -> Self {
68 self.id = Some(client_id.into());
69 self.secret = Some(client_secret.into());
70 self
71 }
72
73 #[must_use]
74 pub const fn environment(mut self, environment: Environment) -> Self {
75 self.environment = environment;
76 self
77 }
78
79 #[must_use]
80 pub fn http_client(mut self, http_client: HttpClient) -> Self {
81 self.http = Some(http_client);
82 self
83 }
84
85 #[must_use]
86 pub fn pkcs12_der(mut self, der: impl Into<Vec<u8>>, password: impl Into<String>) -> Self {
87 self.mtls_source = Some(MtlsSource::Pkcs12Der {
88 der: der.into(),
89 password: password.into(),
90 });
91 self
92 }
93
94 #[must_use]
95 pub fn pkcs12_file(mut self, path: impl Into<PathBuf>, password: impl Into<String>) -> Self {
96 self.mtls_source = Some(MtlsSource::Pkcs12File {
97 path: path.into(),
98 password: password.into(),
99 });
100 self
101 }
102
103 pub fn build(self) -> Result<Client, Error> {
104 let client_id = self.id.ok_or(Error::BuilderMissingField("client_id"))?;
105 let client_secret = self
106 .secret
107 .ok_or(Error::BuilderMissingField("client_secret"))?;
108
109 let http_client = if let Some(http_client) = self.http {
110 if self.mtls_source.is_some() {
111 return Err(Error::BuilderConflict(
112 "cannot set both http_client and pkcs12_* options",
113 ));
114 }
115 http_client
116 } else if let Some(mtls_source) = self.mtls_source {
117 match mtls_source {
118 MtlsSource::Pkcs12Der { der, password } => {
119 let identity = Identity::from_pkcs12_der(&der, &password)?;
120 HttpClient::builder().identity(identity).build()?
121 }
122 MtlsSource::Pkcs12File { path, password } => {
123 let der = fs::read(path)?;
124 let identity = Identity::from_pkcs12_der(&der, &password)?;
125 HttpClient::builder().identity(identity).build()?
126 }
127 }
128 } else {
129 HttpClient::new()
130 };
131
132 Ok(Client::from_parts(
133 client_id,
134 client_secret,
135 self.environment,
136 http_client,
137 ))
138 }
139}
140
141impl Client {
142 const fn from_parts(
143 client_id: String,
144 client_secret: String,
145 environment: Environment,
146 http_client: HttpClient,
147 ) -> Self {
148 Self {
149 id: client_id,
150 secret: client_secret,
151 environment,
152 http: http_client,
153 token: Mutex::new(None),
154 }
155 }
156
157 #[must_use]
158 pub const fn endpoints(&self) -> Endpoints {
159 self.environment.endpoints()
160 }
161
162 pub(crate) async fn send_authenticated<Req, Res>(
163 &self,
164 method: Method,
165 path: &str,
166 payload: Option<&Req>,
167 ) -> Result<Res, Error>
168 where
169 Req: Serialize + Sync,
170 Res: DeserializeOwned,
171 {
172 let token = self.get_valid_access_token().await?;
173 let first_response = self
174 .send_with_token_typed::<Req>(&token, method.clone(), path, payload)
175 .await?;
176
177 if first_response.status() == StatusCode::UNAUTHORIZED {
178 self.authenticate().await?;
179 let refreshed_token = self.get_valid_access_token().await?;
180 let retry_response = self
181 .send_with_token_typed::<Req>(&refreshed_token, method, path, payload)
182 .await?;
183 return Self::parse_response::<Res>(retry_response).await;
184 }
185
186 Self::parse_response::<Res>(first_response).await
187 }
188
189 pub(crate) async fn send_authenticated_billing<Req, Res>(
190 &self,
191 method: Method,
192 path: &str,
193 payload: Option<&Req>,
194 ) -> Result<Res, Error>
195 where
196 Req: Serialize + Sync,
197 Res: DeserializeOwned,
198 {
199 let token = self.get_valid_billing_access_token().await?;
200 let first_response = self
201 .send_with_token_typed_base::<Req>(
202 &token,
203 self.endpoints().billing_api_base_url,
204 method.clone(),
205 path,
206 payload,
207 )
208 .await?;
209
210 if first_response.status() == StatusCode::UNAUTHORIZED {
211 self.authenticate_billing().await?;
212 let refreshed_token = self.get_valid_billing_access_token().await?;
213 let retry_response = self
214 .send_with_token_typed_base::<Req>(
215 &refreshed_token,
216 self.endpoints().billing_api_base_url,
217 method,
218 path,
219 payload,
220 )
221 .await?;
222 return Self::parse_response::<Res>(retry_response).await;
223 }
224
225 Self::parse_response::<Res>(first_response).await
226 }
227
228 async fn send_with_token_typed<Req>(
229 &self,
230 access_token: &str,
231 method: Method,
232 path: &str,
233 payload: Option<&Req>,
234 ) -> Result<reqwest::Response, Error>
235 where
236 Req: Serialize + Sync,
237 {
238 self.send_with_token_typed_base(
239 access_token,
240 self.endpoints().pix_api_base_url,
241 method,
242 path,
243 payload,
244 )
245 .await
246 }
247
248 async fn send_with_token_typed_base<Req>(
249 &self,
250 access_token: &str,
251 base_url: &str,
252 method: Method,
253 path: &str,
254 payload: Option<&Req>,
255 ) -> Result<reqwest::Response, Error>
256 where
257 Req: Serialize + Sync,
258 {
259 let url = format!("{base_url}{path}");
260
261 let mut request = self.http.request(method, url).bearer_auth(access_token);
262
263 if let Some(json_payload) = payload {
264 request = request.json(json_payload);
265 }
266
267 Ok(request.send().await?)
268 }
269
270 async fn parse_response<Res>(response: reqwest::Response) -> Result<Res, Error>
271 where
272 Res: DeserializeOwned,
273 {
274 if !response.status().is_success() {
275 let status = response.status();
276 let body = response.text().await.unwrap_or_else(|_| String::new());
277 return Err(Error::RequestFailed { status, body });
278 }
279
280 let body = response.text().await?;
281 if body.trim().is_empty() {
282 return Err(Error::EmptyResponse);
283 }
284
285 Ok(serde_json::from_str(&body)?)
286 }
287}