1use std::fmt::Debug;
2use std::str::FromStr;
3use std::time::Duration;
4
5use bytes::Bytes;
6use reqwest::{Identity, Response, Url};
7use serde::{de::DeserializeOwned, Serialize};
8use thiserror::Error;
9use tokio::{fs::File, io::AsyncReadExt};
10use tracing::{debug, info, instrument, trace};
11
12use crate::{
13 error::{Error, ErrorResponse},
14 Result,
15};
16
17#[derive(Debug)]
19pub struct RestClientBuilder<'i> {
20 identity_cert_file: &'i str,
21 environment: Environment,
22 connect_timeout: Duration,
23 timeout: Duration,
24 min_tls_version: reqwest::tls::Version,
25}
26
27impl<'i> RestClientBuilder<'i> {
28 #[instrument]
29 pub fn new(identity_cert_file: &'i str, environment: Environment) -> Self {
30 info!(
31 "Configured environment: {environment:?}, connecting to '{}'.",
32 environment.base_url()
33 );
34
35 Self {
36 identity_cert_file,
37 environment,
38 connect_timeout: Duration::from_secs(10),
39 timeout: Duration::from_secs(30),
40 min_tls_version: reqwest::tls::Version::TLS_1_2,
42 }
43 }
44
45 pub fn connect_timeout(&mut self, duration: Duration) -> &mut Self {
47 self.connect_timeout = duration;
48 self
49 }
50
51 pub fn timeout(&mut self, duration: Duration) -> &mut Self {
53 self.timeout = duration;
54 self
55 }
56
57 pub fn min_tls_version(&mut self, version: reqwest::tls::Version) -> &mut Self {
59 self.min_tls_version = version;
60 self
61 }
62
63 #[instrument]
67 pub async fn build(self) -> Result<RestClient> {
68 let mut cert = Vec::new();
69 File::open(self.identity_cert_file)
70 .await
71 .map_err(|source| Error::OpenIdentityCertFile {
72 path: self.identity_cert_file.into(),
73 source,
74 })?
75 .read_to_end(&mut cert)
76 .await
77 .map_err(|source| Error::ReadIdentityCertFile {
78 path: self.identity_cert_file.into(),
79 source,
80 })?;
81 let identity =
82 Identity::from_pem(&cert).map_err(|source| Error::ParseIdentityCertFile {
83 path: self.identity_cert_file.into(),
84 source,
85 })?;
86
87 let client = reqwest::ClientBuilder::new()
88 .identity(identity)
89 .connect_timeout(self.connect_timeout)
90 .timeout(self.timeout)
91 .min_tls_version(self.min_tls_version)
92 .build()
93 .map_err(Error::BuildRequestClient)?;
94
95 Ok(RestClient {
96 client,
97 base_url: self.environment.base_url(),
98 })
99 }
100}
101
102#[derive(Debug, Copy, Clone, Eq, PartialEq)]
109pub enum Environment {
110 Test,
111 Acceptance,
112 Staging,
113 Production,
114}
115
116#[derive(Error, Debug)]
118pub enum ParseEnvironmentError {
119 #[error("'{0}' is not a valid environment string")]
120 InvalidEnvironmentString(String),
121}
122
123impl FromStr for Environment {
124 type Err = ParseEnvironmentError;
125
126 #[instrument]
127 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
128 Ok(match s {
129 "test" => Self::Test,
130 "acceptance" => Self::Acceptance,
131 "staging" => Self::Staging,
132 "production" => Self::Production,
133 s => return Err(ParseEnvironmentError::InvalidEnvironmentString(s.into())),
134 })
135 }
136}
137
138impl Environment {
139 pub fn base_url(&self) -> Url {
140 match self {
141 Environment::Test => "https://test-rest.basispoort.nl/".parse().unwrap(),
142 Environment::Acceptance => "https://acceptatie-rest.basispoort.nl/".parse().unwrap(),
143 Environment::Staging => "https://staging-rest.basispoort.nl/".parse().unwrap(),
144 Environment::Production => "https://rest.basispoort.nl/".parse().unwrap(),
145 }
146 }
147}
148
149#[derive(Clone, Debug)]
150pub struct RestClient {
151 client: reqwest::Client,
152 pub base_url: Url,
153}
154
155impl RestClient {
156 #[instrument]
158 fn make_url(&self, path: &str) -> Result<Url> {
159 self.base_url.join(path).map_err(|source| {
160 Error::ParseUrl {
161 url: path.to_owned(),
162 source,
163 }
164 .into()
165 })
166 }
167
168 #[instrument]
169 async fn error_status(&self, url: &Url, response: Response) -> Result<Response> {
170 let status = response.status();
171
172 debug!(status = status.to_string(), headers = ?response.headers());
173
174 match response.error_for_status_ref() {
175 Err(source) => {
176 let response_bytes = response.bytes().await.map_err(Error::ReceiveResponseBody)?;
177
178 let error_response = match serde_json::from_slice(&response_bytes) {
179 Ok(error_response) => ErrorResponse::JSON(error_response),
180 Err(_) => ErrorResponse::Plain(String::from_utf8_lossy(&response_bytes).into()),
181 };
182 Err(Error::HttpResponse {
183 url: url.to_owned(),
184 status,
185 error_response,
186 source,
187 }
188 .into())
189 }
190 Ok(_) => Ok(response),
191 }
192 }
193
194 #[instrument(skip(self, response))]
195 async fn deserialize<T: DeserializeOwned + Debug>(&self, response: Response) -> Result<T> {
196 let payload_raw = response.bytes().await.map_err(Error::ReceiveResponseBody)?;
197 trace!(?payload_raw);
198
199 let payload_raw = match payload_raw.len() {
201 0 => Bytes::from_static(b"null"),
202 _ => payload_raw,
203 };
204
205 let payload_deserialized =
206 serde_json::from_slice(&payload_raw).map_err(Error::DeserializeResponseBody)?;
207 debug!(?payload_deserialized);
208
209 Ok(payload_deserialized)
210 }
211
212 #[instrument]
213 pub async fn get<T: DeserializeOwned + Debug + ?Sized>(&self, path: &str) -> Result<T> {
214 let url = self.make_url(path)?;
215 trace!("GET {}", url.as_str());
216
217 let response = self
218 .client
219 .get(url.clone())
220 .send()
221 .await
222 .map_err(Error::HttpRequest)?;
223
224 let response = self.error_status(&url, response).await?;
225 self.deserialize(response).await
226 }
227
228 #[instrument(skip(payload))]
229 pub async fn post<P: Serialize + Debug + ?Sized, T: DeserializeOwned + Debug + ?Sized>(
230 &self,
231 path: &str,
232 payload: &P,
233 ) -> Result<T> {
234 let url = self.make_url(path)?;
235 trace!(?payload, "POST {}", url.as_str());
236
237 let response = self
238 .client
239 .post(url.clone())
240 .json(payload)
241 .send()
242 .await
243 .map_err(Error::HttpRequest)?;
244
245 let response = self.error_status(&url, response).await?;
246 self.deserialize(response).await
247 }
248
249 #[instrument(skip(payload))]
250 pub async fn put<P: Serialize + Debug + ?Sized, T: DeserializeOwned + Debug + ?Sized>(
251 &self,
252 path: &str,
253 payload: &P,
254 ) -> Result<T> {
255 let url = self.make_url(path)?;
256 trace!(?payload, "PUT {}", url.as_str());
257
258 let response = self
259 .client
260 .put(url.clone())
261 .json(payload)
262 .send()
263 .await
264 .map_err(Error::HttpRequest)?;
265
266 let response = self.error_status(&url, response).await?;
267 self.deserialize(response).await
268 }
269
270 #[instrument]
271 pub async fn delete<T: DeserializeOwned + Debug + ?Sized>(&self, path: &str) -> Result<T> {
272 let url = self.make_url(path)?;
273 trace!("DELETE {}", url.as_str());
274
275 let response = self
276 .client
277 .delete(url.clone())
278 .send()
279 .await
280 .map_err(Error::HttpRequest)?;
281
282 let response = self.error_status(&url, response).await?;
283 self.deserialize(response).await
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 }