Skip to main content

efi_bank/
client.rs

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}