api_forge/traits/mod.rs
1use crate::error::ApiForgeError;
2use reqwest::header::HeaderMap;
3use serde::de::DeserializeOwned;
4use serde::Serialize;
5use std::fmt::Debug;
6use tracing::{debug, error, info, warn};
7
8/// Enum representing different methods for transmitting data in an HTTP request.
9pub enum DataTransmissionMethod {
10 QueryParams, // Data sent as query parameters.
11 Json, // Data sent as a JSON body.
12 FormData, // Data sent as URL-encoded form data.
13 Multipart, // Data sent as multipart form data.
14}
15
16/// Enum representing different methods for authentication in an HTTP request.
17pub enum AuthenticationMethod {
18 Bearer, // Bearer token authentication.
19 Basic, // Basic authentication (username and password).
20 None, // No authentication.
21}
22
23/// `ApiRequest` trait.
24///
25/// This trait defines a structure for making HTTP requests with custom serialization and response handling.
26/// It is intended to be implemented by request types that are serializable and can generate HTTP requests.
27///
28/// # Requirements
29///
30/// Implementing types must:
31/// - Implement the `Serialize` trait from `serde` for serializing the request data.
32/// - Implement the `Debug` trait for debugging purposes.
33/// - Define a constant `ENDPOINT` representing the API endpoint.
34///
35/// # Associated Constants
36///
37/// - `ENDPOINT`: A static string representing the endpoint for the request.
38/// - `METHOD`: The HTTP method (default is `GET`).
39/// - `DATA_TRANSMISSION_METHOD`: Specifies how the request data is sent (default is `QueryParams`).
40/// - `AUTHENTICATION_METHOD`: Specifies the authentication method (default is `None`).
41///
42/// # Methods
43///
44/// - `generate_request`: Generates a `reqwest::RequestBuilder` based on the request type.
45/// - `send_request`: Sends the request asynchronously and returns the response.
46/// - `send_and_parse`: Sends the request and parses the response, returning a result or an error.
47///
48/// # Example
49///
50/// ```rust
51/// use serde::{Serialize, Deserialize};
52/// use reqwest::header::HeaderMap;
53/// use reqwest::Method;
54/// use api_forge::{ApiRequest, DataTransmissionMethod, AuthenticationMethod, ApiForgeError};
55///
56/// #[derive(Serialize, Debug)]
57/// struct MyRequest {
58/// field1: String,
59/// field2: i32,
60/// }
61///
62/// #[derive(Deserialize, Debug, Default)]
63/// struct MyResponse {
64/// result: String,
65/// }
66///
67/// impl From<reqwest::Response> for MyResponse {
68/// fn from(resp: reqwest::Response) -> Self {
69/// // Convert the response into your response structure
70/// resp.json().unwrap_or_else(|_| MyResponse {
71/// result: "Error parsing response".into(),
72/// })
73/// }
74/// }
75///
76/// impl ApiRequest<MyResponse> for MyRequest {
77/// const ENDPOINT: &'static str = "/api/my_endpoint";
78/// const METHOD: Method = Method::POST; // Override HTTP method if necessary
79/// const DATA_TRANSMISSION_METHOD: DataTransmissionMethod = DataTransmissionMethod::Json; // Send data as JSON
80/// const AUTHENTICATION_METHOD: AuthenticationMethod = AuthenticationMethod::Bearer; // Use Bearer authentication
81/// async fn from_response(resp: reqwest::Response) -> Result<Self::Response, ApiForgeError> where <Self as ApiRequest<MyResponse>>::Response: From<reqwest::Response> {
82/// resp.json().await
83/// }
84/// }
85///
86/// #[tokio::main]
87/// async fn main() {
88/// let request = MyRequest {
89/// field1: "Test".to_string(),
90/// field2: 42,
91/// };
92///
93/// let headers = HeaderMap::new();
94/// let token = Some(("my_token".to_string(), None));
95///
96/// match request.send_and_parse("https://api.example.com", Some(headers), token).await {
97/// Ok(response) => println!("Success: {:?}", response),
98/// Err(e) => eprintln!("Request failed: {:?}", e),
99/// }
100/// }
101/// ```
102#[allow(async_fn_in_trait)]
103pub trait ApiRequest<Res>
104where
105 Self: Serialize + Debug,
106 Res: Default + DeserializeOwned,
107{
108 /// A static string representing the endpoint for the request.
109 const ENDPOINT: &'static str;
110
111 /// Determines the HTTP method for the request. Defaults to `GET`.
112 const METHOD: reqwest::Method = reqwest::Method::GET;
113
114 /// Specifies how the data will be transmitted in the request.
115 /// The default is `DataTransmissionMethod::QueryParams`.
116 const DATA_TRANSMISSION_METHOD: DataTransmissionMethod = DataTransmissionMethod::QueryParams;
117
118 /// Specifies the method of authentication for the request.
119 /// The default is `AuthenticationMethod::None`.
120 const AUTHENTICATION_METHOD: AuthenticationMethod = AuthenticationMethod::None;
121
122 async fn from_response(resp: reqwest::Response) -> Result<Res, ApiForgeError> {
123 // Check for empty body or 204 No Content status
124 if resp.content_length().unwrap_or(0) == 0
125 || resp.status() == reqwest::StatusCode::NO_CONTENT
126 {
127 debug!("Response is empty or 204 No Content.");
128 return Ok(Res::default());
129 }
130
131 // Determine response format based on Content-Type header
132 if let Some(content_type) = resp.headers().get(reqwest::header::CONTENT_TYPE) {
133 let content_type_str = content_type.to_str().unwrap_or("");
134 return if content_type_str.contains("application/json") {
135 debug!("Parsing response as JSON.");
136 resp.json().await.map_err(ApiForgeError::ParseError)
137 } else if content_type_str.contains("text/plain") {
138 error!("Response content type is text/plain, which is not supported.");
139 Err(ApiForgeError::UnsupportedContentType(
140 content_type_str.to_string(),
141 ))
142 } else if content_type_str.contains("application/xml")
143 || content_type_str.contains("text/xml")
144 {
145 debug!("Parsing response as XML.");
146 let text = resp
147 .text()
148 .await
149 .map_err(ApiForgeError::ParseError)?;
150 let xml = serde_xml_rust::from_str(text.as_str())?;
151 Ok(xml)
152 } else {
153 warn!("Unrecognized content type: {}", content_type_str);
154 Err(ApiForgeError::UnsupportedContentType(
155 content_type_str.to_string(),
156 ))
157 };
158 }
159
160 // Default to trying JSON parsing
161 debug!("Falling back to JSON parsing.");
162 resp.json::<Res>()
163 .await
164 .map_err(ApiForgeError::ParseError)
165 }
166
167 /// Optional: Provides multipart form data for file uploads.
168 fn multipart_form_data(&self) -> reqwest::multipart::Form {
169 debug!("Implement multipart_form_data() if needed, or leave empty.");
170 reqwest::multipart::Form::new()
171 }
172
173 /// Generates a `reqwest::RequestBuilder` based on the request's parameters, including optional headers and authentication.
174 fn generate_request(
175 &self,
176 base_url: &str,
177 headers: Option<HeaderMap>,
178 token: Option<(String, Option<String>)>,
179 ) -> reqwest::RequestBuilder {
180 let url = format!("{}{}", base_url, Self::ENDPOINT);
181 let client = reqwest::Client::new();
182
183 // Match the HTTP method
184 let builder = match Self::METHOD {
185 reqwest::Method::GET => client.get(&url),
186 reqwest::Method::POST => client.post(&url),
187 reqwest::Method::PUT => client.put(&url),
188 reqwest::Method::DELETE => client.delete(&url),
189 reqwest::Method::PATCH => client.patch(&url),
190 reqwest::Method::HEAD => client.head(&url),
191 _ => client.get(&url),
192 };
193
194 // Add data based on the transmission method
195 let mut request = match Self::DATA_TRANSMISSION_METHOD {
196 DataTransmissionMethod::QueryParams => builder.query(self),
197 DataTransmissionMethod::Json => builder.json(self),
198 DataTransmissionMethod::FormData => builder.form(self),
199 DataTransmissionMethod::Multipart => builder.multipart(self.multipart_form_data()),
200 };
201
202 // Add authentication if applicable
203 if let Some((token, password)) = token {
204 match Self::AUTHENTICATION_METHOD {
205 AuthenticationMethod::Basic => request = request.basic_auth(token, password),
206 AuthenticationMethod::Bearer => request = request.bearer_auth(token),
207 AuthenticationMethod::None => warn!("No authentication required for this request."),
208 }
209 }
210
211 // Add headers if provided
212 if let Some(headers) = headers {
213 request = request.headers(headers);
214 }
215
216 debug!("Generated request: {:#?}", request);
217 request
218 }
219
220 /// Sends the request asynchronously and returns the result.
221 async fn send_request(
222 &self,
223 base_url: &str,
224 headers: Option<HeaderMap>,
225 token: Option<(String, Option<String>)>,
226 ) -> reqwest::Result<reqwest::Response> {
227 info!("Sending request to {}{}...", base_url, Self::ENDPOINT);
228 debug!("Request body: {:#?}", self);
229 self.generate_request(base_url, headers, token).send().await
230 }
231
232 /// Sends the request and attempts to parse the response.
233 /// Returns a `Result` containing the parsed response or an error.
234 async fn send_and_parse(
235 &self,
236 base_url: &str,
237 headers: Option<HeaderMap>,
238 token: Option<(String, Option<String>)>,
239 ) -> Result<Res, ApiForgeError> {
240 let response = self.send_request(base_url, headers, token).await?;
241
242 if response.error_for_status_ref().is_err() {
243 Err(ApiForgeError::ResponseError(response.status()))
244 } else {
245 Ok(Self::from_response(response).await?)
246 }
247 }
248}