cloudflare_but_works/framework/client/
async_api.rs1use crate::framework::client::ClientConfig;
2use crate::framework::endpoint::{EndpointSpec, MultipartPart, RequestBody};
3use crate::framework::response::ResponseConverter;
4use crate::framework::{
5 auth::{AuthClient, Credentials},
6 response::ApiResponse,
7 response::{ApiErrors, ApiFailure, ApiSuccess},
8 Environment,
9};
10use std::borrow::Cow;
11use std::net::SocketAddr;
12
13pub struct Client {
16 environment: Environment,
17 credentials: Credentials,
18 http_client: reqwest::Client,
19}
20
21impl AuthClient for reqwest::RequestBuilder {
22 fn auth(mut self, credentials: &Credentials) -> Self {
23 for (k, v) in credentials.headers() {
24 self = self.header(k, v);
25 }
26 self
27 }
28}
29
30impl Client {
31 pub fn new(
32 credentials: Credentials,
33 config: ClientConfig,
34 environment: Environment,
35 ) -> Result<Client, crate::framework::Error> {
36 let mut builder = reqwest::Client::builder().default_headers(config.default_headers);
37
38 #[cfg(not(target_arch = "wasm32"))]
39 {
40 if let Some(address) = config.resolve_ip {
42 let url = url::Url::from(&environment);
43 builder = builder.resolve(
44 url.host_str()
45 .expect("Environment url should have a hostname"),
46 SocketAddr::new(address, 443),
47 );
48 }
49
50 builder = builder.timeout(config.http_timeout);
52 }
53
54 let http_client = builder.build()?;
55
56 Ok(Client {
57 environment,
58 credentials,
59 http_client,
60 })
61 }
62
63 pub async fn request<Endpoint>(
66 &self,
67 endpoint: &Endpoint,
68 ) -> ApiResponse<Endpoint::ResponseType>
69 where
70 Endpoint: EndpointSpec + Send + Sync,
71 Endpoint::ResponseType: ResponseConverter<Endpoint::JsonResponse>,
72 {
73 let mut request = self
75 .http_client
76 .request(endpoint.method(), endpoint.url(&self.environment));
77
78 if let Some(body) = endpoint.body() {
79 match body {
80 RequestBody::Json(json) => {
81 request = request.body(json);
82 }
83 RequestBody::Raw(bytes) => {
84 request = request.body(bytes);
85 }
86 RequestBody::MultiPart(multipart) => {
87 let mut form = reqwest::multipart::Form::new();
88 for (name, part) in multipart.parts() {
89 match part {
90 MultipartPart::Text(text) => {
91 form = form.text(name, text);
92 }
93 MultipartPart::Bytes(bytes) => {
94 form = form.part(name, reqwest::multipart::Part::bytes(bytes));
95 }
96 }
97 }
98 request = request.multipart(form);
99 }
100 }
101 match endpoint.content_type() {
103 None | Some(Cow::Borrowed("multipart/form-data")) => {}
104 Some(content_type) => {
105 request = request.header(reqwest::header::CONTENT_TYPE, content_type.as_ref());
106 }
107 }
108 }
109
110 request = request.auth(&self.credentials);
111 let response = request.send().await?;
112
113 if Endpoint::IS_RAW_BODY {
116 map_api_response_raw::<Endpoint>(response).await
117 } else {
118 map_api_response_json::<Endpoint>(response).await
119 }
120 }
121}
122
123async fn map_api_response_raw<Endpoint>(
127 resp: reqwest::Response,
128) -> Result<Endpoint::ResponseType, ApiFailure>
129where
130 Endpoint: EndpointSpec,
131 Endpoint::ResponseType: ResponseConverter<Endpoint::JsonResponse>,
132{
133 let status = resp.status();
134 if status.is_success() {
135 let bytes = resp.bytes().await.map_err(ApiFailure::Invalid)?.to_vec();
136 Ok(Endpoint::ResponseType::from_raw(bytes))
137 } else {
138 let parsed: Result<ApiErrors, reqwest::Error> = resp.json().await;
139 let errors = parsed.unwrap_or_default();
140 Err(ApiFailure::Error(status, errors))
141 }
142}
143
144async fn map_api_response_json<Endpoint>(
145 resp: reqwest::Response,
146) -> Result<Endpoint::ResponseType, ApiFailure>
147where
148 Endpoint: EndpointSpec,
149 Endpoint::ResponseType: ResponseConverter<Endpoint::JsonResponse>,
150{
151 let status = resp.status();
152 if status.is_success() {
153 let parsed: Result<ApiSuccess<Endpoint::JsonResponse>, reqwest::Error> = resp.json().await;
154 match parsed {
155 Ok(success) => Ok(Endpoint::ResponseType::from_json(success)),
156 Err(e) => Err(ApiFailure::Invalid(e)),
157 }
158 } else {
159 let parsed: Result<ApiErrors, reqwest::Error> = resp.json().await;
160 let errors = parsed.unwrap_or_default();
161 Err(ApiFailure::Error(status, errors))
162 }
163}
164
165#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::framework::auth::Credentials;
170 use crate::framework::client::ClientConfig;
171 use crate::framework::endpoint::RequestBody;
172 use crate::framework::endpoint::{serialize_query, EndpointSpec};
173 use crate::framework::response::{ApiFailure, ApiResult, ApiSuccess};
174 use crate::framework::Environment;
175 use mockito::{Matcher, Server};
176 use regex;
177 use regex::Regex;
178 use serde::{Deserialize, Serialize};
179 use serde_json::json;
180 use tokio;
181
182 #[derive(Debug)]
184 struct DummyJsonEndpoint;
185
186 #[derive(Debug, Deserialize)]
187 struct DummyJsonResponse {
188 message: String,
189 }
190
191 impl ApiResult for DummyJsonResponse {}
192
193 impl EndpointSpec for DummyJsonEndpoint {
194 type JsonResponse = DummyJsonResponse;
195 type ResponseType = ApiSuccess<Self::JsonResponse>;
196
197 fn method(&self) -> reqwest::Method {
198 reqwest::Method::GET
199 }
200
201 fn path(&self) -> String {
202 "/dummy/json".into()
203 }
204 }
205 #[derive(Debug)]
209 struct DummyRawEndpoint;
210
211 impl EndpointSpec for DummyRawEndpoint {
212 const IS_RAW_BODY: bool = true;
213 type JsonResponse = ();
214 type ResponseType = Vec<u8>;
215
216 fn method(&self) -> reqwest::Method {
217 reqwest::Method::GET
218 }
219
220 fn path(&self) -> String {
221 "/dummy/raw".into()
222 }
223 }
224 #[derive(Debug)]
228 struct DummyNothingEndpoint;
229
230 impl EndpointSpec for DummyNothingEndpoint {
231 type JsonResponse = ();
232 type ResponseType = ApiSuccess<Self::JsonResponse>;
233
234 fn method(&self) -> reqwest::Method {
235 reqwest::Method::GET
236 }
237
238 fn path(&self) -> String {
239 "/dummy/nothing".into()
240 }
241 }
242 #[derive(Debug)]
246 struct DummyJsonRequestEndpoint;
247
248 impl EndpointSpec for DummyJsonRequestEndpoint {
249 type JsonResponse = ();
250 type ResponseType = ApiSuccess<Self::JsonResponse>;
251
252 fn method(&self) -> reqwest::Method {
253 reqwest::Method::POST
254 }
255
256 fn path(&self) -> String {
257 "/dummy/json".into()
258 }
259
260 fn body(&self) -> Option<RequestBody> {
261 Some(RequestBody::Json(json!({"key": "value"}).to_string()))
262 }
263 }
264 #[derive(Debug)]
268 struct DummyRawRequestEndpoint;
269
270 impl EndpointSpec for DummyRawRequestEndpoint {
271 const IS_RAW_BODY: bool = true;
272 type JsonResponse = ();
273 type ResponseType = Vec<u8>;
274
275 fn method(&self) -> reqwest::Method {
276 reqwest::Method::POST
277 }
278
279 fn path(&self) -> String {
280 "/dummy/raw".into()
281 }
282
283 fn body(&self) -> Option<RequestBody> {
284 Some(RequestBody::Raw(b"raw content".to_vec()))
285 }
286 }
287 #[derive(Debug)]
291 struct DummyMultipartEndpoint;
292
293 impl EndpointSpec for DummyMultipartEndpoint {
294 type JsonResponse = ();
295 type ResponseType = ApiSuccess<Self::JsonResponse>;
296
297 fn method(&self) -> reqwest::Method {
298 reqwest::Method::POST
299 }
300
301 fn path(&self) -> String {
302 "/dummy/multipart".into()
303 }
304
305 fn body(&self) -> Option<RequestBody> {
306 Some(RequestBody::MultiPart(&DummyMultipart))
307 }
308 }
309
310 struct DummyMultipart;
311
312 impl crate::framework::endpoint::MultipartBody for DummyMultipart {
313 fn parts(&self) -> Vec<(String, MultipartPart)> {
314 vec![("key".into(), MultipartPart::Text("value".into()))]
315 }
316 }
317 #[derive(Debug)]
321 struct DummyJsonRequestWithQueryEndpoint;
322
323 #[derive(Debug, Serialize)]
324 struct DummyJsonRequestWithQueryParams {
325 key: String,
326 }
327
328 impl EndpointSpec for DummyJsonRequestWithQueryEndpoint {
329 type JsonResponse = ();
330 type ResponseType = ApiSuccess<Self::JsonResponse>;
331
332 fn method(&self) -> reqwest::Method {
333 reqwest::Method::POST
334 }
335
336 fn path(&self) -> String {
337 "/dummy/json".into()
338 }
339
340 fn query(&self) -> Option<String> {
341 serialize_query(&DummyJsonRequestWithQueryParams {
342 key: "value".into(),
343 })
344 }
345 }
346 fn create_test_client(url: String) -> Client {
349 let environment = Environment::Custom(url);
350 let credentials = Credentials::UserAuthToken {
351 token: "dummy".into(),
352 };
353 let config = ClientConfig::default();
354 Client::new(credentials, config, environment).unwrap()
355 }
356
357 #[tokio::test]
359 async fn test_json_endpoint_success() {
360 let body = json!({
361 "result": {"message": "Hello, World!"},
362 "result_info": null,
363 "messages": [],
364 "errors": [],
365 "success": true
366 });
367
368 let mut server = Server::new_async().await;
369 let mock = server
370 .mock("GET", "/dummy/json")
371 .with_status(200)
372 .with_header("content-type", "application/json")
373 .with_body(body.to_string())
374 .match_header("content-type", Matcher::Missing)
375 .match_query(Matcher::Missing)
376 .match_body(Matcher::Missing)
377 .create();
378
379 let client = create_test_client(server.url());
380 let response = client.request(&DummyJsonEndpoint).await;
381
382 mock.assert();
383 let response = response.unwrap();
384 assert_eq!(response.result.message, "Hello, World!");
385 assert_eq!(response.result_info, None);
386 assert!(response.messages.is_empty());
387 assert!(response.errors.is_empty());
388 }
389
390 #[tokio::test]
392 async fn test_raw_endpoint_success() {
393 let raw_body = b"raw content".to_vec();
394
395 let mut server = Server::new_async().await;
396 let mock = server
397 .mock("GET", "/dummy/raw")
398 .with_status(200)
399 .with_header("content-type", "application/octet-stream")
400 .with_body(raw_body.clone())
401 .match_header("content-type", Matcher::Missing)
402 .match_query(Matcher::Missing)
403 .match_body(Matcher::Missing)
404 .create();
405
406 let client = create_test_client(server.url());
407 let response = client.request(&DummyRawEndpoint).await.unwrap();
408
409 mock.assert();
410 assert_eq!(response, raw_body);
411 }
412
413 #[tokio::test]
415 async fn test_endpoint_failure() {
416 let body = json!({
417 "errors": [{"code": 123, "message": "Something went wrong", "other": {}}],
418 "other": {}
419 });
420
421 let mut server = Server::new_async().await;
422 let mock = server
423 .mock("GET", "/dummy/json")
424 .with_status(400)
425 .with_header("content-type", "application/json")
426 .with_body(body.to_string())
427 .match_header("content-type", Matcher::Missing)
428 .match_query(Matcher::Missing)
429 .match_body(Matcher::Missing)
430 .create();
431
432 let client = create_test_client(server.url());
433 let result = client.request(&DummyJsonEndpoint).await;
434
435 mock.assert();
436 assert!(result.is_err());
437 if let Err(ApiFailure::Error(status, errors)) = result {
438 assert_eq!(status.as_u16(), 400);
439 assert!(!errors.errors.is_empty());
440 assert_eq!(errors.errors[0].code, 123);
441 } else {
442 panic!("Expected error result");
443 }
444 }
445
446 #[tokio::test]
448 async fn test_nothing_endpoint_success() {
449 let body = json!({
450 "result": null,
451 "result_info": null,
452 "messages": [],
453 "errors": [],
454 "success": true
455 });
456
457 let mut server = Server::new_async().await;
458 let mock = server
459 .mock("GET", "/dummy/nothing")
460 .with_status(200)
461 .with_header("content-type", "application/json")
462 .with_body(body.to_string())
463 .match_header("content-type", Matcher::Missing)
464 .match_query(Matcher::Missing)
465 .match_body(Matcher::Missing)
466 .create();
467
468 let client = create_test_client(server.url());
469 let response = client.request(&DummyNothingEndpoint).await;
470
471 mock.assert();
472 let response = response.unwrap();
473 assert!(matches!(response.result, ()));
474 assert_eq!(response.result_info, None);
475 assert!(response.messages.is_empty());
476 assert!(response.errors.is_empty());
477 }
478
479 #[tokio::test]
481 async fn test_json_body_success() {
482 let body = json!({
483 "result": null,
484 "result_info": null,
485 "messages": [],
486 "errors": [],
487 "success": true
488 });
489
490 let mut server = Server::new_async().await;
491 let mock = server
492 .mock("POST", "/dummy/json")
493 .with_status(200)
494 .with_header("content-type", "application/json")
495 .with_body(body.to_string())
496 .match_header("content-type", "application/json")
497 .match_query(Matcher::Missing)
498 .match_body(Matcher::Json(json!({"key": "value"})))
499 .create();
500
501 let client = create_test_client(server.url());
502 let _ = client.request(&DummyJsonRequestEndpoint).await;
503
504 mock.assert();
505 }
506
507 #[tokio::test]
509 async fn test_raw_body_success() {
510 let raw_body = b"raw content".to_vec();
511
512 let mut server = Server::new_async().await;
513 let mock = server
514 .mock("POST", "/dummy/raw")
515 .with_status(200)
516 .with_header("content-type", "application/octet-stream")
517 .with_body(raw_body.clone())
518 .match_header("content-type", "application/octet-stream")
519 .match_query(Matcher::Missing)
520 .match_body(raw_body)
521 .create();
522
523 let client = create_test_client(server.url());
524 let _ = client.request(&DummyRawRequestEndpoint).await;
525
526 mock.assert();
527 }
528
529 #[tokio::test]
531 async fn test_multipart_body_success() {
532 let body = json!({
533 "result": null,
534 "result_info": null,
535 "messages": [],
536 "errors": [],
537 "success": true
538 });
539
540 let mut server = Server::new_async().await;
541
542 let mock = server
543 .mock("POST", "/dummy/multipart")
544 .with_status(200)
545 .with_header("content-type", "application/json")
546 .with_body(body.to_string())
547 .match_header(
548 "content-type",
549 Matcher::Regex("multipart/form-data; boundary=.*".into()),
550 )
551 .match_query(Matcher::Missing)
552 .match_request(|req| {
553 let body = req.body().unwrap().to_vec();
554 let body = String::from_utf8_lossy(&body);
555
556 let re = Regex::new(
557 r#"^--.*\s+Content-Disposition: form-data; name="key"\s+\s+value\s+--.*\s*$"#,
558 )
559 .unwrap();
560 re.is_match(&body)
561 })
562 .create();
563
564 let client = create_test_client(server.url());
565 let _ = client.request(&DummyMultipartEndpoint).await;
566
567 mock.assert();
568 }
569
570 #[tokio::test]
572 async fn test_query_parameters_success() {
573 let body = json!({
574 "result": null,
575 "result_info": null,
576 "messages": [],
577 "errors": [],
578 "success": true
579 });
580
581 let mut server = Server::new_async().await;
582 let mock = server
583 .mock("POST", "/dummy/json")
584 .with_status(200)
585 .with_header("content-type", "application/json")
586 .with_body(body.to_string())
587 .match_header("content-type", Matcher::Missing)
588 .match_query(Matcher::UrlEncoded("key".into(), "value".into()))
589 .match_body(Matcher::Missing)
590 .create();
591
592 let client = create_test_client(server.url());
593 let _ = client.request(&DummyJsonRequestWithQueryEndpoint).await;
594
595 mock.assert();
596 }
597}