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}