Skip to main content

scraper_trail/request/
mod.rs

1use bounded_static::{IntoBoundedStatic, ToBoundedStatic};
2use chrono::{DateTime, Utc};
3use http::{
4    Method,
5    header::{HeaderMap, HeaderName, HeaderValue},
6};
7use indexmap::IndexMap;
8use serde_field_attributes::{represented_as_str, timestamp_millis_str};
9use std::borrow::Cow;
10use url::Url;
11
12pub mod params;
13
14#[derive(Debug, thiserror::Error)]
15pub enum HeaderError {
16    #[error("Invalid header name")]
17    Name(#[from] http::header::InvalidHeaderName),
18    #[error("Invalid header value")]
19    Value(#[from] http::header::InvalidHeaderValue),
20}
21
22#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
23pub struct Request<'a> {
24    pub url: Url,
25    #[serde(rename = "timestamp_ms", with = "timestamp_millis_str")]
26    pub timestamp: DateTime<Utc>,
27    #[serde(
28        with = "represented_as_str",
29        default,
30        skip_serializing_if = "is_method_get"
31    )]
32    pub method: Method,
33    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
34    pub headers: IndexMap<Cow<'a, str>, Cow<'a, str>>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub body: Option<Cow<'a, str>>,
37}
38
39impl<'a> Request<'a> {
40    pub fn new<
41        U: AsRef<str>,
42        K: Into<Cow<'a, str>>,
43        V: Into<Cow<'a, str>>,
44        I: IntoIterator<Item = (K, V)>,
45        B: Into<Cow<'a, str>>,
46    >(
47        url: U,
48        timestamp: Option<DateTime<Utc>>,
49        method: Option<Method>,
50        headers: Option<I>,
51        body: Option<B>,
52    ) -> Result<Self, url::ParseError> {
53        Ok(Self {
54            url: url.as_ref().parse()?,
55            timestamp: timestamp.unwrap_or_else(Utc::now),
56            method: method.unwrap_or_default(),
57            headers: headers
58                .map(|headers| {
59                    headers
60                        .into_iter()
61                        .map(|(key, value)| (key.into(), value.into()))
62                        .collect()
63                })
64                .unwrap_or_default(),
65            body: body.map(std::convert::Into::into),
66        })
67    }
68
69    pub fn header_map(&self) -> Result<HeaderMap, HeaderError> {
70        self.headers
71            .iter()
72            .map(|(name, value)| {
73                Ok((
74                    HeaderName::try_from(name.as_ref())?,
75                    HeaderValue::try_from(value.as_ref())?,
76                ))
77            })
78            .collect()
79    }
80}
81
82impl IntoBoundedStatic for Request<'_> {
83    type Static = Request<'static>;
84
85    fn into_static(self) -> Self::Static {
86        Self::Static {
87            url: self.url,
88            timestamp: self.timestamp,
89            method: self.method,
90            headers: self
91                .headers
92                .into_iter()
93                .map(|(key, value)| (key.into_static(), value.into_static()))
94                .collect(),
95            body: self
96                .body
97                .map(bounded_static::IntoBoundedStatic::into_static),
98        }
99    }
100}
101
102impl ToBoundedStatic for Request<'_> {
103    type Static = Request<'static>;
104
105    fn to_static(&self) -> Self::Static {
106        Self::Static {
107            url: self.url.clone(),
108            timestamp: self.timestamp,
109            method: self.method.clone(),
110            headers: self
111                .headers
112                .iter()
113                .map(|(key, value)| (key.to_static(), value.to_static()))
114                .collect(),
115            body: self
116                .body
117                .as_ref()
118                .map(bounded_static::ToBoundedStatic::to_static),
119        }
120    }
121}
122
123fn is_method_get(method: &Method) -> bool {
124    method == Method::GET
125}