url_cleaner_engine/glue/
http.rs

1//! HTTP requests.
2
3use std::collections::HashMap;
4
5use url::Url;
6use serde::{Deserialize, Serialize};
7use reqwest::{Method, header::{HeaderName, HeaderValue}};
8use thiserror::Error;
9#[expect(unused_imports, reason = "Used in a doc comment.")]
10use reqwest::cookie::Cookie;
11use serde_with::{serde_as, DisplayFromStr};
12
13use crate::types::*;
14use crate::glue::*;
15use crate::util::*;
16
17/// Rules for making an HTTP request.
18///
19/// Currently only capable of making blocking requests.
20#[serde_as]
21#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize, Suitability)]
22#[serde(deny_unknown_fields)]
23pub struct RequestConfig {
24    /// The URL to send the request to.
25    ///
26    /// Defaults to [`StringSource::Part`]`(`[`UrlPart::Whole`]`)`.
27    #[serde(default = "get_string_source_part_whole", skip_serializing_if = "is_string_source_part_whole")]
28    pub url: StringSource,
29    /// The method to use.
30    ///
31    /// Defaults to [`Method::GET`].
32    #[serde_as(as = "DisplayFromStr")]
33    #[serde(default, skip_serializing_if = "is_default")]
34    pub method: Method,
35    /// The headers to send in addition to the default headers from the [`HttpClientConfig`] and [`Self::client_config_diff`].
36    ///
37    /// If a call to [`StringSource::get`] returns [`None`], the header it came from isn't sent. This can be useful for API keys.
38    ///
39    /// Defaults to an empty set.
40    #[serde(default, skip_serializing_if = "is_default")]
41    pub headers: HashMap<String, StringSource>,
42    /// The body to send.
43    ///
44    /// Defaults to [`None`].
45    #[serde(default, skip_serializing_if = "is_default")]
46    pub body: Option<RequestBody>,
47    /// What to part of the response to return.
48    ///
49    /// Defaults to [`ResponseHandler::Body`].
50    #[serde(default, skip_serializing_if = "is_default")]
51    pub response_handler: ResponseHandler,
52    /// Overrides for the [`HttpClientConfig`] this uses to make the [`reqwest::blocking::Client`].
53    #[serde(default, skip_serializing_if = "is_default")]
54    pub client_config_diff: Option<HttpClientConfigDiff>
55}
56
57/// Serde helper function for [`RequestConfig::url`].
58fn get_string_source_part_whole() -> StringSource {StringSource::Part(UrlPart::Whole)}
59/// Serde helper function for [`RequestConfig::url`].
60fn is_string_source_part_whole(value: &StringSource) -> bool {value == &get_string_source_part_whole()}
61
62/// The enum of errors [`RequestConfig::make`] can return.
63#[derive(Debug, Error)]
64pub enum MakeHttpRequestError {
65    /// Returned when a [`reqwest::Error`] is encountered.
66    #[error(transparent)]
67    ReqwestError(#[from] reqwest::Error),
68    /// Returned when a [`RequestBodyError`] is encountered.
69    #[error(transparent)]
70    RequestBodyError(#[from] RequestBodyError),
71    /// Returned when a call to [`StringSource::get`] returns [`None`] where it has to return [`Some`].
72    #[error("A StringSource was None where it has to be Some.")]
73    StringSourceIsNone,
74    /// Returned when a [`StringSourceError`] is encountered.
75    #[error(transparent)]
76    StringSourceError(#[from] Box<StringSourceError>),
77    /// Returned when a [`url::ParseError`] is encountered.
78    #[error(transparent)]
79    UrlParseError(#[from] url::ParseError),
80    /// Returned when a [`ResponseHandlerError`] is encountered.
81    #[error(transparent)]
82    ResponseHandlerError(#[from] ResponseHandlerError),
83    /// Returned when a [`reqwest::header::InvalidHeaderName`] is encountered.
84    #[error(transparent)]
85    InvalidHeaderName(#[from] reqwest::header::InvalidHeaderName),
86    /// Returned when a [`reqwest::header::InvalidHeaderValue`] is encountered.
87    #[error(transparent)]
88    InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue)
89}
90
91/// The enum of errors [`RequestConfig::send`] can return.
92#[derive(Debug, Error)]
93pub enum SendHttpRequestError {
94    /// Returned when a [`MakeHttpRequestError`] is encountered.
95    #[error(transparent)]
96    MakeHttpRequestError(#[from] MakeHttpRequestError),
97    /// Returned when a [`reqwest::Error`] is encountered.
98    #[error(transparent)]
99    ReqwestError(#[from] reqwest::Error)
100}
101
102/// The enum of errors [`RequestConfig::response`] can return.
103#[derive(Debug, Error)]
104pub enum HttpResponseError {
105    /// Returned when a [`SendHttpRequestError`] is encountered.
106    #[error(transparent)]
107    SendHttpRequestError(#[from] SendHttpRequestError),
108    /// Returned when a [`reqwest::Error`] is encountered.
109    #[error(transparent)]
110    ReqwestError(#[from] reqwest::Error),
111    /// Returned when a [`ResponseHandlerError`] is encountered.
112    #[error(transparent)]
113    ResponseHandlerError(#[from] ResponseHandlerError)
114}
115
116impl From<StringSourceError> for MakeHttpRequestError {
117    fn from(value: StringSourceError) -> Self {
118        Self::StringSourceError(Box::new(value))
119    }
120}
121
122impl RequestConfig {
123    /// Makes the request.
124    /// # Errors
125    /// If the call to [`TaskStateView::http_client`] returns an error, that error is returned.
126    ///
127    /// If [`Self::url`]'s call to [`StringSource::get`] returns an error, that error is returned.
128    ///
129    /// If [`Self::url`]'s call to [`StringSource::get`] returns [`None`], returns the error [`MakeHttpRequestError::StringSourceIsNone`].
130    ///
131    /// If any of [`Self::headers`]'s calls to [`StringSource::get`] return an error, that error is returned.
132    ///
133    /// If any of [`Self::headers`]'s calls to [`HeaderName::try_from`] returns an error, that error is returned.
134    ///
135    /// If the call to [`RequestBody::apply`] returns an error, that error is returned.
136    pub fn make(&self, task_state: &TaskStateView) -> Result<reqwest::blocking::RequestBuilder, MakeHttpRequestError> {
137        let mut ret=task_state.http_client(self.client_config_diff.as_ref())?
138            .request(
139                self.method.clone(),
140                Url::parse(get_str!(self.url, task_state, MakeHttpRequestError))?,
141            );
142        for (name, value) in self.headers.iter() {
143            if let Some(value) = value.get(task_state)? {
144                ret = ret.header(HeaderName::try_from(name)?, HeaderValue::try_from(value.into_owned())?);
145            }
146        }
147        if let Some(body) = &self.body {ret=body.apply(ret, task_state)?;}
148        Ok(ret)
149    }
150
151    /// Makes and sends the request.
152    /// # Errors
153    /// If the call to [`Self::make`] returns an error, that error is returned.
154    ///
155    /// If the call to [`reqwest::blocking::RequestBuilder::send`] returns an error, that error is returned.
156    pub fn send(&self, task_state: &TaskStateView) -> Result<reqwest::blocking::Response, SendHttpRequestError> {
157        Ok(self.make(task_state)?.send()?)
158    }
159
160    /// Make the request, send it, and return the response specified by [`Self::response_handler`].
161    /// # Errors
162    /// If the call to [`Self::send`] returns an error, that error is returned.
163    ///
164    /// If the call to [`RequestHandler::handle`} returns an error, that error is returned.
165    pub fn response(&self, task_state: &TaskStateView) -> Result<String, HttpResponseError> {
166        Ok(self.response_handler.handle(self.send(task_state)?, task_state)?)
167    }
168}
169
170/// How a [`RequestConfig`] should construct its body.
171#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Suitability)]
172#[serde(deny_unknown_fields)]
173pub enum RequestBody {
174    /// Send the specified text.
175    /// # Errors
176    /// If the call to [`StringSource::get`] returns an error, that error is returned.
177    ///
178    /// If the call to [`StringSource::get`] returns [`None`], returns the error [`RequestBodyError::StringSourceIsNone`].
179    Text(StringSource),
180    /// Sends the HTML form.
181    ///
182    /// If a call to [`StringSource::get`] returns [`None`], the field it came from isn't sent. This can be useful for API keys.
183    /// # Errors
184    /// If a call to [`StringSource::get`] returns an error, that error is returned.
185    Form(HashMap<String, StringSource>),
186    /// Sends JSON.
187    /// # Errors
188    /// If the call to [`StringSourceJsonValue::make`] returns an error, that error is returned.
189    Json(StringSourceJsonValue)
190}
191
192/// The enum of errors [`RequestBody::apply`] can return.
193#[derive(Debug, Error)]
194pub enum RequestBodyError {
195    /// Returned when a [`StringSourceError`] is encountered.
196    #[error(transparent)]
197    StringSourceError(Box<StringSourceError>),
198    /// Returned when a call to [`StringSource::get`] returns [`None`] where it must return [`Some`].
199    #[error("A StringSource was None where it has to be Some.")]
200    StringSourceIsNone
201}
202
203impl From<StringSourceError> for RequestBodyError {
204    fn from(value: StringSourceError) -> Self {
205        Self::StringSourceError(Box::new(value))
206    }
207}
208
209impl RequestBody {
210    /// Inserts the specified body into a [`reqwest::blocking::RequestBuilder`].
211    /// # Errors
212    /// See each variant of [`Self`] for when each variant returns an error.
213    pub fn apply(&self, request: reqwest::blocking::RequestBuilder, task_state: &TaskStateView) -> Result<reqwest::blocking::RequestBuilder, RequestBodyError> {
214        Ok(match self {
215            Self::Text(StringSource::String(value)) => request.body(value.clone()),
216            Self::Text(value) => request.body(get_string!(value, task_state, RequestBodyError)),
217            Self::Form(map) => {
218                let mut ret = HashMap::new();
219                for (k, v) in map.iter() {
220                    if let Some(v) = v.get(task_state)? {
221                        ret.insert(k, v);
222                    }
223                }
224                request.form(&ret)
225            },
226            Self::Json(json) => request.json(&json.make(task_state)?)
227        })
228    }
229}
230
231/// What part of a response a [`RequestConfig`] should return.
232///
233/// Defaults to [`Self::Body`].
234#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Suitability)]
235#[serde(deny_unknown_fields)]
236pub enum ResponseHandler {
237    /// Get the response body.
238    /// # Errors
239    /// If the call to [`reqwest::blocking::Response::text`] returns an error, that error is returned.
240    #[default]
241    Body,
242    /// Get the specified header.
243    /// # Errors
244    /// If the call to [`StringSource::get`] returns an error, that error is returned.
245    ///
246    /// If the call to [`StringSource::get`] returns [`None`], returns the error [`ResponseHandlerError::StringSourceIsNone`].
247    ///
248    /// If the header isn't found, returns the error [`ResponseHandlerError::HeaderNotFound`].
249    ///
250    /// If the call to [`HeaderValue::to_str`] returns an error, that error is returned.
251    Header(StringSource),
252    /// Get the final URL.
253    Url,
254    /// Get the specified cookie.
255    /// # Errors
256    /// If the call to [`StringSource::get`] returns an error, that error is returned.
257    ///
258    /// If the call to [`StringSource::get`] returns [`None`], returns the error [`ResponseHandlerError::CookieNotFound`].
259    Cookie(StringSource)
260}
261
262/// The enum of errors [`ResponseHandler::handle`] can return.
263#[derive(Debug, Error)]
264pub enum ResponseHandlerError {
265    /// Returned when a [`reqwest::Error`] is encountered.
266    #[error(transparent)]
267    ReqwestError(#[from] reqwest::Error),
268    /// Returned when a [`StringSourceError`] is encountered.
269    #[error(transparent)]
270    StringSourceError(Box<StringSourceError>),
271    /// Returned when a call to [`StringSource::get`] returns [`None`] where it has to return [`Some`].
272    #[error("A StringSource was None where it has to be Some.")]
273    StringSourceIsNone,
274    /// Returned when a requested header isn't found.
275    #[error("The requested header was not found.")]
276    HeaderNotFound,
277    /// Returned when a [`reqwest::header::ToStrError`] is encountered.
278    #[error(transparent)]
279    ToStrError(#[from] reqwest::header::ToStrError),
280    /// Returned when a requested cookie isn't found.
281    #[error("The requested cookie was not found.")]
282    CookieNotFound
283}
284
285impl From<StringSourceError> for ResponseHandlerError {
286    fn from(value: StringSourceError) -> Self {
287        Self::StringSourceError(Box::new(value))
288    }
289}
290
291impl ResponseHandler {
292    /// Gets the specified part of a [`reqwest::blocking::Response`].
293    /// # Errors
294    /// See each variant of [`Self`] for when each variant returns an error.
295    pub fn handle(&self, response: reqwest::blocking::Response, task_state: &TaskStateView) -> Result<String, ResponseHandlerError> {
296        Ok(match self {
297            Self::Body => response.text()?,
298            Self::Header(name) => response.headers().get(get_str!(name, task_state, ResponseHandlerError)).ok_or(ResponseHandlerError::HeaderNotFound)?.to_str()?.to_string(),
299            Self::Url => response.url().as_str().to_string(),
300            Self::Cookie(source) => {
301                let name = get_string!(source, task_state, ResponseHandlerError);
302                response.cookies().find(|cookie| cookie.name()==name).ok_or(ResponseHandlerError::CookieNotFound)?.value().to_string()
303            }
304        })
305    }
306}