api_request_utils/lib.rs
1#![doc = include_str!("../README.md")]
2#![forbid(missing_docs)]
3
4use std::collections::HashMap;
5
6use reqwest::{
7 Client,
8 RequestBuilder,
9};
10
11use serde_json::Value;
12use serde::de::DeserializeOwned;
13
14use async_trait::async_trait;
15
16use thiserror::Error as ErrorMacro;
17
18pub use reqwest;
19pub use serde_json;
20pub use serde;
21pub use ::async_trait;
22
23/// Trait to provide some basic info about API
24pub trait RequestInfo {
25 /// The base URL for the requests.
26 const BASE_URL : &'static str;
27
28 /// Returns the [reqwest::Client] instance associated with the API client.
29 ///
30 /// The client is used to send HTTP requests to the API.
31 fn client(&self) -> &Client;
32}
33
34/// This trait provides methods for modifying the struct in a specific way:
35pub trait RequestModifiers: RequestInfo {
36 /// Joins the given endpoint with the base URL.
37 ///
38 /// # Arguments
39 ///
40 /// * `endpoint` - The endpoint to join with the base URL.
41 ///
42 /// # Returns
43 ///
44 /// The joined URL as a `String`.
45 fn create_endpoint(endpoint : &str) -> String {
46 format!("{}/{}",Self::BASE_URL,endpoint)
47 }
48
49 /// Conditionally adds a header to the given `RequestBuilder` based on the result of a closure.
50 ///
51 /// If the closure returns `true`, the specified header with the provided `key` and `value`
52 /// will be added to the request. If the closure returns `false`, no changes will be made
53 /// to the request.
54 ///
55 /// # Arguments
56 ///
57 /// * `request_builder` - The `RequestBuilder` to add the header to.
58 /// * `key` - The key of the header to be added.
59 /// * `value` - The value of the header to be added.
60 /// * `closure` - A closure that determines whether the header should be added. It should
61 /// take no arguments and return a boolean value.
62 ///
63 /// # Returns
64 ///
65 /// The modified `RequestBuilder` with the header added if the closure returns `true`,
66 /// otherwise the original `RequestBuilder` without any modifications.
67 fn add_header_if(request_builder: RequestBuilder,key: &str, value: &str,closure : impl FnOnce() -> bool) -> RequestBuilder {
68 match closure() {
69 true => request_builder.header(key, value),
70 false => request_builder
71 }
72 }
73}
74
75/// The RequestDefaults trait provides default methods for configuring and modifying HTTP requests.
76pub trait RequestDefaults: RequestModifiers {
77 /// Modifies the provided `RequestBuilder` with default headers.
78 ///
79 /// # Arguments
80 ///
81 /// * `request_builder` - The `RequestBuilder` to modify.
82 ///
83 /// # Returns
84 ///
85 /// The modified `RequestBuilder` with default headers set.
86 fn default_headers(&self,request_builder : reqwest::RequestBuilder) -> reqwest::RequestBuilder {
87 request_builder
88 }
89
90 /// Modifies the provided `RequestBuilder` with default parameters.
91 ///
92 /// # Arguments
93 ///
94 /// * `request_builder` - The `RequestBuilder` to modify.
95 ///
96 /// # Returns
97 ///
98 /// The modified `RequestBuilder` with default parameters set.
99 fn default_parameters(&self,request_builder : reqwest::RequestBuilder) -> reqwest::RequestBuilder {
100 request_builder
101 }
102
103 /// Modifies the provided `RequestBuilder` with default settings for post request.
104 ///
105 /// # Arguments
106 ///
107 /// * `endpoint` - The endpoint for the request.
108 /// * `json` - The JSON payload for the request.
109 ///
110 /// # Returns
111 ///
112 /// The modified `RequestBuilder` with default settings applied.
113 fn default_post_requestor(&self,endpoint : &str, json : String) -> reqwest::RequestBuilder {
114 self.default_parameters(self.default_headers(self.client().post(Self::create_endpoint(endpoint)))).body(json)
115 }
116
117 /// Modifies the provided `RequestBuilder` with default settings for get request.
118 ///
119 /// # Arguments
120 ///
121 /// * `endpoint` - The endpoint for the request.
122 /// * `parameters` - The Parameters for the request.
123 ///
124 /// # Returns
125 ///
126 /// The modified `RequestBuilder` with default settings applied.
127 fn default_get_requestor<'a>(&self,endpoint : &str,parameters : &HashMap<&'a str,Value>) -> reqwest::RequestBuilder {
128 self.default_parameters(self.default_headers(self.client().get(Self::create_endpoint(endpoint)))).query(¶meters)
129 }
130}
131
132
133/// A trait for handling HTTP requests.
134#[async_trait]
135pub trait RequestHandler<T : DeserializeOwned,O : DeserializeOwned,E : DeserializeOwned> : RequestDefaults {
136 /// Sends an HTTP request, processes the response, and maps it using the provided closure.
137 ///
138 /// This asynchronous function sends an HTTP request using the given `reqwest::RequestBuilder`,
139 /// processes the response, and maps it using the provided closure. It returns the mapped
140 /// result if the request is successful, or an `RequestError::ErrorPayload` variant if the
141 /// request fails.
142 ///
143 /// # Arguments
144 ///
145 /// * `self` - A reference to the struct implementing this trait.
146 /// * `request` - The `reqwest::RequestBuilder` representing the request to be sent.
147 /// * `map` - A closure that maps the successful response JSON into the desired output type. Just write `|x| x` if the not mapping is required.
148 ///
149 /// # Returns
150 ///
151 /// A `Result` containing the mapped output type or an `RequestError` variant.
152 async fn request_map(request: reqwest::RequestBuilder,map : impl FnOnce(T) -> O + Send + Sync) -> Result<O,RequestError<E>> {
153 let response = request.send().await?;
154 let status = response.status();
155
156 let body = response.bytes().await?;
157
158 match status.is_success() {
159 true => {
160 let json = serde_json::from_slice(&body)?;
161 Ok(map(json))
162 }
163 false => {
164 let json = serde_json::from_slice(&body)?;
165 Err(RequestError::ErrorPayload(json))
166 }
167 }
168 }
169
170 /// Resolves the error in the response and returns an option containing the value or `None`.
171 ///
172 /// # Arguments
173 ///
174 /// * `response` - The response as a `Result` type.
175 /// * `error_resolver` - The closure that handles the error and performs custom error handling.
176 ///
177 /// # Returns
178 ///
179 /// An option containing the value if the response is successful, otherwise `None`.
180 fn resolve_error(&self,response : Result<O,RequestError<E>>,error_handler : impl Fn(RequestError<E>) + Sync) -> Option<O> {
181 match response {
182 Ok(value) => Some(value),
183 Err(error) => {
184 error_handler(error);
185 None
186 }
187 }
188 }
189
190 /// This asynchronous function constructs (by default) a GET request using the `default_get_requestor` method
191 /// with the given endpoint and parameters. It then sends the request using the request method, expecting
192 /// a response of type `T` or an error of type `E`. The error is resolved using the `resolve_error` method
193 /// and returns an `Option<T>` representing the response data if successful, or `None` if an error occurred.
194 ///
195 /// # Arguments
196 ///
197 /// * `endpoint` - The endpoint URL to send the GET request to.
198 /// * `parameters` - A hashmap containing any parameters to include in the request.
199 /// * `map` - A closure that maps the successful response JSON into the desired output type.
200 /// * `error_handler` - A closure that handles the error case if an `RequestError` occurs.
201 ///
202 /// # Returns
203 ///
204 /// An `Option<O>` representing the response data if successful, or `None` if an error occurred.
205 async fn get_request_handler<'a>(&self,endpoint : &str,parameters : &HashMap<&'a str,Value>,map : impl FnOnce(T) -> O + Send + Sync,error_handler : impl Fn(RequestError<E>) + Sync + Send) -> Option<O> {
206 let request = self.default_get_requestor(endpoint,parameters);
207 let response = Self::request_map(request,map).await;
208 self.resolve_error(response,error_handler)
209 }
210
211 /// Handles a POST request to the specified endpoint with the provided JSON payload and returns the response data of type T.
212 ///
213 /// This asynchronous function constructs a POST request using the `default_post_requestor` method with the given endpoint
214 /// and JSON payload. It then sends the request using the request method, expecting a response of type `T` or an error of type `E`.
215 /// The error is resolved using the `resolve_error` method and returns an `Option<T>` representing the response data if successful,
216 /// or `None` if an error occurred.
217 ///
218 /// # Arguments
219 ///
220 /// * `endpoint` - The endpoint URL to send the POST request to.
221 /// * `json` - A string containing the JSON payload to include in the request.
222 /// * `map` - A closure that maps the successful response JSON into the desired output type.
223 /// * `error_handler` - A closure that handles the error case if an `RequestError` occurs.
224 ///
225 /// # Returns
226 ///
227 /// An `Option<O>` representing the response data if successful, or `None` if an error occurred.
228 async fn post_request_handler(&self,endpoint : &str,json : String,map : impl FnOnce(T) -> O + Send + Sync,error_handler : impl Fn(RequestError<E>) + Sync + Send) -> Option<O> {
229 let request = self.default_post_requestor(endpoint,json);
230 let response = Self::request_map(request,map).await;
231 self.resolve_error(response,error_handler)
232 }
233}
234
235
236/// Enum representing different types of HTTPS errors.
237#[derive(ErrorMacro)]
238pub enum RequestError<E> {
239 /// Error that occurs when sending a request.
240 #[error(
241r#"Failed operation relating to request to ({}) with status code of {}.
242Is request: {},
243Is connect: {},
244Is body: {}"#,
245.0.url().map(|x|x.to_string()).unwrap_or(String::from("Not Found")),
246.0.status().map(|x|x.to_string()).unwrap_or(String::from("Not Found")),
247.0.is_request(),
248.0.is_connect(),
249.0.is_body()
250)]
251 RequestError(#[from] reqwest::Error),
252
253 #[error("Failed to parse json due to {}",.0)]
254 /// Error indicating invalid JSON body during deserialization.
255 InvalidJsonBody(#[from] serde_json::Error),
256
257 /// Error payload (json) when request is not successful
258 #[error("Request error playload : {0}")]
259 ErrorPayload(#[source] E),
260}