melvin_ob/http_handler/http_request/
request_common.rs

1use super::response_common::{HTTPResponseType, ResponseError};
2use crate::http_handler::{HTTPError, http_client::HTTPClient};
3use std::{fmt::Debug, io::ErrorKind};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use strum_macros::Display;
7
8/// Base trait for all types representing HTTP requests.
9///
10/// Each implementor must define the associated response type and required metadata
11/// such as endpoint, HTTP method, headers, and query parameters.
12pub(crate) trait HTTPRequestType {
13    /// The type of the expected HTTP response.
14    type Response: HTTPResponseType;
15
16    /// Returns the request endpoint path, relative to the client's base URL.
17    fn endpoint(&self) -> &str;
18
19    /// Specifies the HTTP request method (GET, POST, etc.).
20    fn request_method(&self) -> HTTPRequestMethod;
21
22    /// Provides custom request headers. Defaults to an empty map.
23    fn header_params(&self) -> reqwest::header::HeaderMap {
24        reqwest::header::HeaderMap::default()
25    }
26
27    /// Provides URL query parameters. Defaults to an empty map.
28    fn query_params(&self) -> HashMap<&str, String> {
29        HashMap::new()
30    }
31
32    /// Creates the base `RequestBuilder` from the HTTP client, applying method and URL.
33    ///
34    /// # Arguments
35    /// * `client` – The preconfigured `HTTPClient` used to perform the request.
36    ///
37    /// # Returns
38    /// * A `reqwest::RequestBuilder` ready for customization (headers, body, etc.).
39    fn get_request_base(&self, client: &HTTPClient) -> reqwest::RequestBuilder {
40        let compound_url = format!("{}{}", client.url(), self.endpoint());
41        match self.request_method() {
42            HTTPRequestMethod::Get => client.client().get(compound_url),
43            HTTPRequestMethod::Post => client.client().post(compound_url),
44            HTTPRequestMethod::Put => client.client().put(compound_url),
45            HTTPRequestMethod::Delete => client.client().delete(compound_url),
46        }
47    }
48}
49
50
51/// Enum representing the four primary HTTP request methods.
52#[derive(Debug)]
53pub(crate) enum HTTPRequestMethod {
54    /// HTTP GET request.
55    Get,
56    /// HTTP POST request.
57    Post,
58    /// HTTP PUT request.
59    Put,
60    /// HTTP DELETE request.
61    Delete,
62}
63
64
65/// Errors that may occur while constructing or performing an HTTP request.
66#[derive(Debug, Display)]
67pub(crate) enum RequestError {
68    /// The requested file or resource could not be found.
69    NotFound,
70    /// A file or resource could not be opened due to permissions.
71    FailedToOpen,
72    /// An unknown or unspecified request error occurred.
73    Unknown,
74}
75
76impl std::error::Error for RequestError {}
77
78impl From<std::io::Error> for RequestError {
79    /// Maps standard I/O errors to [`RequestError`] variants.
80    fn from(value: std::io::Error) -> Self {
81        match value.kind() {
82            ErrorKind::NotFound => RequestError::NotFound,
83            ErrorKind::PermissionDenied => RequestError::FailedToOpen,
84            _ => RequestError::Unknown,
85        }
86    }
87}
88
89/// Trait for request types that send a JSON body and expect a structured response.
90///
91/// Requires a `Body` type implementing `serde::Serialize`.
92pub(crate) trait JSONBodyHTTPRequestType: HTTPRequestType {
93    /// Type of the serializable request body.
94    type Body: serde::Serialize;
95
96    /// Returns a reference to the request body.
97    fn body(&self) -> &Self::Body;
98
99    /// Constructs a header map with `"Content-Type: application/json"` pre-filled.
100    fn header_params_with_content_type(&self) -> reqwest::header::HeaderMap {
101        let mut headers = self.header_params();
102        headers.append(
103            "Content-Type",
104            reqwest::header::HeaderValue::from_static("application/json"),
105        );
106        headers
107    }
108
109    /// Sends the request with a JSON-encoded body.
110    ///
111    /// # Arguments
112    /// * `client` – The shared HTTP client instance.
113    ///
114    /// # Returns
115    /// * Parsed response value or an `HTTPError`.
116    async fn send_request(
117        &self,
118        client: &HTTPClient,
119    ) -> Result<<Self::Response as HTTPResponseType>::ParsedResponseType, HTTPError> {
120        let response = self
121            .get_request_base(client)
122            .headers(self.header_params_with_content_type())
123            .query(&self.query_params())
124            .json(&self.body())
125            .send()
126            .await;
127        let resp = response.map_err(ResponseError::from);
128        Self::Response::read_response(resp.map_err(HTTPError::HTTPResponseError)?)
129            .await
130            .map_err(HTTPError::HTTPResponseError)
131    }
132}
133
134
135/// Trait for requests that do not include a request body.
136pub(crate) trait NoBodyHTTPRequestType: HTTPRequestType {
137    /// Sends a request with no body.
138    ///
139    /// # Arguments
140    /// * `client` – The HTTP client instance.
141    ///
142    /// # Returns
143    /// * Parsed response value or an `HTTPError`.
144    async fn send_request(
145        &self,
146        client: &HTTPClient,
147    ) -> Result<<Self::Response as HTTPResponseType>::ParsedResponseType, HTTPError> {
148        let response = self
149            .get_request_base(client)
150            .headers(self.header_params())
151            .query(&self.query_params())
152            .send()
153            .await;
154        let resp = response.map_err(ResponseError::from);
155        Self::Response::read_response(resp.map_err(HTTPError::HTTPResponseError)?)
156            .await
157            .map_err(HTTPError::HTTPResponseError)
158    }
159}
160
161/// Trait for requests that send multipart form data (e.g., file uploads).
162///
163/// Requires a file path to construct a `multipart/form-data` body.
164pub(crate) trait MultipartBodyHTTPRequestType: HTTPRequestType {
165    /// Assembles the multipart form body from the image path.
166    ///
167    /// # Returns
168    /// * A multipart form with the image file attached.
169    async fn body(&self) -> Result<reqwest::multipart::Form, RequestError> {
170        let file_part = reqwest::multipart::Part::file(self.image_path()).await?;
171        Ok(reqwest::multipart::Form::new().part("image", file_part))
172    }
173
174    /// Returns the absolute or relative path to the image file.
175    fn image_path(&self) -> &PathBuf;
176
177    /// Sends the multipart form request.
178    ///
179    /// # Arguments
180    /// * `client` – The HTTP client instance.
181    ///
182    /// # Returns
183    /// * Parsed response value or an `HTTPError`.
184    async fn send_request(
185        &self,
186        client: &HTTPClient,
187    ) -> Result<<Self::Response as HTTPResponseType>::ParsedResponseType, HTTPError> {
188        let response = self
189            .get_request_base(client)
190            .headers(self.header_params())
191            .query(&self.query_params())
192            .multipart(self.body().await.map_err(HTTPError::HTTPRequestError)?)
193            .send()
194            .await;
195        let resp = response.map_err(ResponseError::from);
196        Self::Response::read_response(resp.map_err(HTTPError::HTTPResponseError)?)
197            .await
198            .map_err(HTTPError::HTTPResponseError)
199    }
200}
201
202/// Converts a `bool` value to a string slice (`"true"` or `"false"`).
203///
204/// Useful for generating query parameters.
205///
206/// # Arguments
207/// * `value` – Boolean value to stringify.
208///
209/// # Returns
210/// * `"true"` or `"false"` as `&'static str`.
211pub(super) fn bool_to_string(value: bool) -> &'static str {
212    if value { "true" } else { "false" }
213}