1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
use crate::error::ApiForgeError;
use reqwest::header::HeaderMap;
use serde::Serialize;
use std::fmt::Debug;
use tracing::{debug, info, warn};

/// Enum representing different methods for transmitting data in an HTTP request.
pub enum DataTransmissionMethod {
    QueryParams, // Data sent as query parameters.
    Json,        // Data sent as a JSON body.
    FormData,    // Data sent as URL-encoded form data.
    Multipart,   // Data sent as multipart form data.
}

/// Enum representing different methods for authentication in an HTTP request.
pub enum AuthenticationMethod {
    Bearer, // Bearer token authentication.
    Basic,  // Basic authentication (username and password).
    None,   // No authentication.
}

/// `Request` trait.
///
/// This trait defines a structure for making HTTP requests with custom serialization and response handling.
/// It is intended to be implemented by request types that are serializable and can generate HTTP requests.
///
/// # Requirements
///
/// Implementing types must:
/// - Implement the `Serialize` trait from `serde` for serializing the request data.
/// - Implement the `Debug` trait for debugging purposes.
/// - Define a constant `ENDPOINT` representing the API endpoint.
///
/// # Associated Constants
///
/// - `ENDPOINT`: A static string representing the endpoint for the request.
/// - `METHOD`: The HTTP method (default is `GET`).
/// - `DATA_TRANSMISSION_METHOD`: Specifies how the request data is sent (default is `QueryParams`).
/// - `AUTHENTICATION_METHOD`: Specifies the authentication method (default is `None`).
///
/// # Methods
///
/// - `generate_request`: Generates a `reqwest::RequestBuilder` based on the request type.
/// - `send_request`: Sends the request asynchronously and returns the response.
/// - `send_and_parse`: Sends the request and parses the response, returning a result or an error.
///
/// # Example
///
/// ```rust
/// use serde::{Serialize, Deserialize};
/// use reqwest::header::HeaderMap;
/// use reqwest::Method;
/// use api_forge::{Request, DataTransmissionMethod, AuthenticationMethod, ApiForgeError};
///
/// #[derive(Serialize, Debug)]
/// struct MyRequest {
///     field1: String,
///     field2: i32,
/// }
///
/// #[derive(Deserialize, Debug, Default)]
/// struct MyResponse {
///     result: String,
/// }
///
/// impl From<reqwest::Response> for MyResponse {
///     fn from(resp: reqwest::Response) -> Self {
///         // Convert the response into your response structure
///         resp.json().unwrap_or_else(|_| MyResponse {
///             result: "Error parsing response".into(),
///         })
///     }
/// }
///
/// impl Request<MyResponse> for MyRequest {
///     const ENDPOINT: &'static str = "/api/my_endpoint";
///     const METHOD: Method = Method::POST; // Override HTTP method if necessary
///     const DATA_TRANSMISSION_METHOD: DataTransmissionMethod = DataTransmissionMethod::Json; // Send data as JSON
///     const AUTHENTICATION_METHOD: AuthenticationMethod = AuthenticationMethod::Bearer; // Use Bearer authentication
///     async fn from_response(resp: reqwest::Response) -> Result<Self::Response, ApiForgeError> where <Self as Request<MyResponse>>::Response: From<reqwest::Response> {
///         resp.json().await
///     }
/// }
///
/// #[tokio::main]
/// async fn main() {
///     let request = MyRequest {
///         field1: "Test".to_string(),
///         field2: 42,
///     };
///
///     let headers = HeaderMap::new();
///     let token = Some(("my_token".to_string(), None));
///
///     match request.send_and_parse("https://api.example.com", Some(headers), token).await {
///         Ok(response) => println!("Success: {:?}", response),
///         Err(e) => eprintln!("Request failed: {:?}", e),
///     }
/// }
/// ```
#[allow(async_fn_in_trait)]
pub trait Request<Res>
where
    Self: Serialize + Debug,
{
    /// The type of the response, which must implement `From<reqwest::Response>`.
    type Response = Res;

    /// A static string representing the endpoint for the request.
    const ENDPOINT: &'static str;

    /// Determines the HTTP method for the request. Defaults to `GET`.
    const METHOD: reqwest::Method = reqwest::Method::GET;

    /// Specifies how the data will be transmitted in the request.
    /// The default is `DataTransmissionMethod::QueryParams`.
    const DATA_TRANSMISSION_METHOD: DataTransmissionMethod = DataTransmissionMethod::QueryParams;

    /// Specifies the method of authentication for the request.
    /// The default is `AuthenticationMethod::None`.
    const AUTHENTICATION_METHOD: AuthenticationMethod = AuthenticationMethod::None;

    async fn from_response(resp: reqwest::Response) -> Result<Self::Response, ApiForgeError>;

    /// Optional: Provides multipart form data for file uploads.
    fn multipart_form_data(&self) -> reqwest::multipart::Form {
        debug!("Implement multipart_form_data() if needed, or leave empty.");
        reqwest::multipart::Form::new()
    }

    /// Generates a `reqwest::RequestBuilder` based on the request's parameters, including optional headers and authentication.
    fn generate_request(
        &self,
        base_url: &str,
        headers: Option<HeaderMap>,
        token: Option<(String, Option<String>)>,
    ) -> reqwest::RequestBuilder {
        let url = format!("{}{}", base_url, Self::ENDPOINT);
        let client = reqwest::Client::new();

        // Match the HTTP method
        let builder = match Self::METHOD {
            reqwest::Method::GET => client.get(&url),
            reqwest::Method::POST => client.post(&url),
            reqwest::Method::PUT => client.put(&url),
            reqwest::Method::DELETE => client.delete(&url),
            reqwest::Method::PATCH => client.patch(&url),
            reqwest::Method::HEAD => client.head(&url),
            _ => client.get(&url),
        };

        // Add data based on the transmission method
        let mut request = match Self::DATA_TRANSMISSION_METHOD {
            DataTransmissionMethod::QueryParams => builder.query(self),
            DataTransmissionMethod::Json => builder.json(self),
            DataTransmissionMethod::FormData => builder.form(self),
            DataTransmissionMethod::Multipart => builder.multipart(self.multipart_form_data()),
        };

        // Add authentication if applicable
        if let Some((token, password)) = token {
            match Self::AUTHENTICATION_METHOD {
                AuthenticationMethod::Basic => request = request.basic_auth(token, password),
                AuthenticationMethod::Bearer => request = request.bearer_auth(token),
                AuthenticationMethod::None => warn!("No authentication required for this request."),
            }
        }

        // Add headers if provided
        if let Some(headers) = headers {
            request = request.headers(headers);
        }

        debug!("Generated request: {:#?}", request);
        request
    }

    /// Sends the request asynchronously and returns the result.
    async fn send_request(
        &self,
        base_url: &str,
        headers: Option<HeaderMap>,
        token: Option<(String, Option<String>)>,
    ) -> reqwest::Result<reqwest::Response> {
        info!("Sending request to {}{}...", base_url, Self::ENDPOINT);
        debug!("Request body: {:#?}", self);
        self.generate_request(base_url, headers, token).send().await
    }

    /// Sends the request and attempts to parse the response.
    /// Returns a `Result` containing the parsed response or an error.
    async fn send_and_parse(
        &self,
        base_url: &str,
        headers: Option<HeaderMap>,
        token: Option<(String, Option<String>)>,
    ) -> Result<Self::Response, ApiForgeError> {
        let response = self.send_request(base_url, headers, token).await?;

        if response.error_for_status_ref().is_err() {
            Err(ApiForgeError::ResponseError(response.status()))
        } else {
            Ok(Self::from_response(response).await?)
        }
    }
}