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}