wappu/engine/
client.rs

1use reqwest::{
2    self,
3    header::{HeaderMap, HeaderValue, SET_COOKIE},
4    Response, StatusCode,
5};
6use serde::de::DeserializeOwned;
7use std::{collections::HashMap, error::Error};
8
9#[derive(Debug)]
10pub enum WappuError {
11    Network(reqwest::Error),
12    UnexpectedStatusCode(reqwest::StatusCode, String),
13    CapmonsterError(String),
14}
15
16impl std::fmt::Display for WappuError {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match *self {
19            WappuError::Network(ref err) => write!(f, "Network error: {}", err),
20            WappuError::UnexpectedStatusCode(ref code, ref text) => {
21                write!(f, "Unexpected status code: {}. Response text: {}", code, text)
22            }
23            WappuError::CapmonsterError(ref err) => write!(f, "Capmonster error: {}", err),
24        }
25    }
26}
27
28impl From<serde_json::Error> for WappuError {
29    fn from(err: serde_json::Error) -> WappuError {
30        WappuError::CapmonsterError(format!("JSON deserialization error: {}", err))
31    }
32}
33
34#[macro_export]
35macro_rules! headers {
36    ($($key:expr => $value:expr),* $(,)?) => {{
37        let mut map = reqwest::header::HeaderMap::new();
38        $(
39            map.insert($key, $value.parse().unwrap());
40        )*
41        map
42    }};
43}
44
45#[macro_export]
46macro_rules! query_params {
47    ($($key:expr => $value:expr),* $(,)?) => {{
48        let mut params = Vec::new();
49        $(
50            params.push((String::from($key), String::from($value)));
51        )*
52        params
53    }};
54}
55
56impl Error for WappuError {}
57
58impl From<reqwest::Error> for WappuError {
59    fn from(err: reqwest::Error) -> WappuError {
60        WappuError::Network(err)
61    }
62}
63
64pub struct WappuClient {
65    client: reqwest::Client,
66    query_params: Vec<(String, String)>,
67}
68
69impl WappuClient {
70    pub fn new() -> Self {
71        WappuClient {
72            client: reqwest::Client::new(),
73            query_params: Vec::new(),
74        }
75    }
76
77    pub fn query_params(mut self, params: Vec<(String, String)>) -> Self {
78        self.query_params = params;
79        self
80    }
81
82    pub async fn get(
83        &self,
84        url: &str,
85        headers: Option<HeaderMap>,
86    ) -> Result<WappuResponse, WappuError> {
87        let request = self.client.get(url);
88
89        let request = if !self.query_params.is_empty() {
90            request.query(&self.query_params)
91        } else {
92            request
93        };
94
95        let response = self.send_request(request, headers).await?;
96        WappuResponse::from_response(response).await
97    }
98
99    pub async fn post(
100        &self,
101        url: &str,
102        body: &str,
103        headers: Option<HeaderMap>,
104    ) -> Result<WappuResponse, WappuError> {
105        let request = self.client.post(url).body(body.to_string());
106
107        let request = if !self.query_params.is_empty() {
108            request.query(&self.query_params)
109        } else {
110            request
111        };
112
113        let response = self.send_request(request, headers).await?;
114        WappuResponse::from_response(response).await
115    }
116
117    pub async fn put(
118        &self,
119        url: &str,
120        body: &str,
121        headers: Option<HeaderMap>,
122    ) -> Result<WappuResponse, WappuError> {
123        let request = self.client.put(url).body(body.to_string());
124
125        let request = if !self.query_params.is_empty() {
126            request.query(&self.query_params)
127        } else {
128            request
129        };
130
131        let response = self.send_request(request, headers).await?;
132        WappuResponse::from_response(response).await
133    }
134
135    pub async fn delete(
136        &self,
137        url: &str,
138        headers: Option<HeaderMap>,
139    ) -> Result<WappuResponse, WappuError> {
140        let request = self.client.delete(url);
141
142        let request = if !self.query_params.is_empty() {
143            request.query(&self.query_params)
144        } else {
145            request
146        };
147
148        let response = self.send_request(request, headers).await?;
149        WappuResponse::from_response(response).await
150    }
151
152    pub async fn head(
153        &self,
154        url: &str,
155        headers: Option<HeaderMap>,
156    ) -> Result<WappuResponse, WappuError> {
157        let request = self.client.head(url);
158
159        let request = if !self.query_params.is_empty() {
160            request.query(&self.query_params)
161        } else {
162            request
163        };
164
165        let response = self.send_request(request, headers).await?;
166        WappuResponse::from_response(response).await
167    }
168
169    pub async fn patch(
170        &self,
171        url: &str,
172        body: &str,
173        headers: Option<HeaderMap>,
174    ) -> Result<WappuResponse, WappuError> {
175        let request = self.client.patch(url).body(body.to_string());
176
177        let request = if !self.query_params.is_empty() {
178            request.query(&self.query_params)
179        } else {
180            request
181        };
182
183        let response = self.send_request(request, headers).await?;
184        WappuResponse::from_response(response).await
185    }
186
187    async fn send_request(
188        &self,
189        request: reqwest::RequestBuilder,
190        headers: Option<HeaderMap>,
191    ) -> Result<Response, WappuError> {
192        let mut request = request;
193        if let Some(h) = headers {
194            request = request.headers(h);
195        }
196        let response = request.send().await?;
197        if !response.status().is_success() {
198            let status_code = response.status();
199            let response_text = response.text().await.map_err(WappuError::from)?;
200            return Err(WappuError::UnexpectedStatusCode(status_code, response_text));
201        }
202        Ok(response)
203    }
204}
205
206pub struct WappuResponse {
207    text: String,
208    headers: HeaderMap,
209    status_code: StatusCode,
210    cookies: HashMap<String, String>, // Cookies represented as a key-value pair for simplicity
211}
212
213impl WappuResponse {
214    // Creates a new WappuResponse from a reqwest::Response, fetching text, headers, and cookies asynchronously
215    async fn from_response(response: Response) -> Result<Self, WappuError> {
216        let status_code = response.status();
217        let headers = response.headers().clone(); // Clone headers before consuming response
218
219        // Attempt to extract cookies from headers before calling response.text()
220        let cookies = headers
221            .get_all(SET_COOKIE)
222            .iter()
223            .filter_map(|header_value| parse_cookie(header_value))
224            .collect();
225
226        let body_text = response.text().await.map_err(WappuError::from)?;
227
228        Ok(WappuResponse {
229            text: body_text,
230            headers,
231            status_code,
232            cookies,
233        })
234    }
235
236    // Method to get the response text without consuming the response
237    pub fn text(&self) -> &str {
238        &self.text
239    }
240
241    // Method to get headers
242    pub fn headers(&self) -> &HeaderMap {
243        &self.headers
244    }
245
246    // Method to get status code
247    pub fn status_code(&self) -> StatusCode {
248        self.status_code
249    }
250
251    // Method to get cookies
252    pub fn cookies(&self) -> &HashMap<String, String> {
253        &self.cookies
254    }
255
256    pub async fn json<T: DeserializeOwned>(&self) -> Result<T, WappuError> {
257        serde_json::from_str(&self.text).map_err(WappuError::from)
258    }
259}
260
261// Utility function to parse a cookie from a Set-Cookie header value
262fn parse_cookie(header_value: &HeaderValue) -> Option<(String, String)> {
263    header_value.to_str().ok().and_then(|cookie_str| {
264        let parts: Vec<&str> = cookie_str.splitn(2, '=').collect();
265        if parts.len() == 2 {
266            Some((
267                parts[0].trim().to_string(),
268                parts[1].split(';').next()?.trim().to_string(),
269            ))
270        } else {
271            None
272        }
273    })
274}