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(&parameters)
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}