viewpoint_core/network/route_builders/
mod.rs

1//! Builder types for route handling.
2//!
3//! This module provides builders for fulfilling and continuing intercepted
4//! network requests. The `FetchBuilder` and `FetchedResponse` types are in
5//! the `route_fetch` module.
6
7use std::future::Future;
8use std::path::Path;
9use std::pin::Pin;
10
11use viewpoint_cdp::protocol::fetch::{ContinueRequestParams, FulfillRequestParams, HeaderEntry};
12
13use super::route::Route;
14use super::route_fetch::FetchedResponse;
15use crate::error::NetworkError;
16
17/// Builder for fulfilling a request with a custom response.
18#[derive(Debug)]
19pub struct FulfillBuilder<'a> {
20    pub(super) route: &'a Route,
21    pub(super) status: u16,
22    pub(super) status_text: Option<String>,
23    pub(super) headers: Vec<HeaderEntry>,
24    pub(super) body: Option<Vec<u8>>,
25}
26
27impl<'a> FulfillBuilder<'a> {
28    pub(super) fn new(route: &'a Route) -> Self {
29        Self {
30            route,
31            status: 200,
32            status_text: None,
33            headers: Vec::new(),
34            body: None,
35        }
36    }
37
38    /// Set the HTTP status code.
39    #[must_use]
40    pub fn status(mut self, code: u16) -> Self {
41        self.status = code;
42        self
43    }
44
45    /// Set the HTTP status text.
46    #[must_use]
47    pub fn status_text(mut self, text: impl Into<String>) -> Self {
48        self.status_text = Some(text.into());
49        self
50    }
51
52    /// Set a response header.
53    #[must_use]
54    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
55        self.headers.push(HeaderEntry {
56            name: name.into(),
57            value: value.into(),
58        });
59        self
60    }
61
62    /// Set multiple response headers.
63    #[must_use]
64    pub fn headers(
65        mut self,
66        headers: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
67    ) -> Self {
68        for (name, value) in headers {
69            self.headers.push(HeaderEntry {
70                name: name.into(),
71                value: value.into(),
72            });
73        }
74        self
75    }
76
77    /// Set the Content-Type header.
78    #[must_use]
79    pub fn content_type(self, mime_type: impl Into<String>) -> Self {
80        self.header("Content-Type", mime_type)
81    }
82
83    /// Set the response body as text.
84    #[must_use]
85    pub fn body(mut self, body: impl Into<String>) -> Self {
86        self.body = Some(body.into().into_bytes());
87        self
88    }
89
90    /// Set the response body as bytes.
91    #[must_use]
92    pub fn body_bytes(mut self, body: impl Into<Vec<u8>>) -> Self {
93        self.body = Some(body.into());
94        self
95    }
96
97    /// Set the response body as JSON.
98    #[must_use]
99    pub fn json<T: serde::Serialize>(self, value: &T) -> Self {
100        let json = serde_json::to_string(value).unwrap_or_default();
101        self.content_type("application/json").body(json)
102    }
103
104    /// Set the response body from a file.
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if the file cannot be read.
109    pub async fn path(mut self, path: impl AsRef<Path>) -> Result<Self, NetworkError> {
110        let body = tokio::fs::read(path.as_ref())
111            .await
112            .map_err(|e| NetworkError::IoError(e.to_string()))?;
113        self.body = Some(body);
114
115        // Try to set content type based on file extension
116        if let Some(ext) = path.as_ref().extension().and_then(|e| e.to_str()) {
117            let mime_type = match ext.to_lowercase().as_str() {
118                "html" | "htm" => "text/html",
119                "css" => "text/css",
120                "js" => "application/javascript",
121                "json" => "application/json",
122                "png" => "image/png",
123                "jpg" | "jpeg" => "image/jpeg",
124                "gif" => "image/gif",
125                "svg" => "image/svg+xml",
126                "pdf" => "application/pdf",
127                "txt" => "text/plain",
128                "xml" => "application/xml",
129                _ => "application/octet-stream",
130            };
131            return Ok(self.content_type(mime_type));
132        }
133
134        Ok(self)
135    }
136
137    /// Modify an existing response.
138    ///
139    /// Use this to fulfill with a response that was fetched via `route.fetch()`.
140    #[must_use]
141    pub fn response(mut self, response: &FetchedResponse<'_>) -> Self {
142        self.status = response.status;
143        for (name, value) in &response.headers {
144            self.headers.push(HeaderEntry {
145                name: name.clone(),
146                value: value.clone(),
147            });
148        }
149        // Include the body from the fetched response
150        if let Some(ref body) = response.body {
151            self.body = Some(body.clone());
152        }
153        self
154    }
155
156    /// Send the response.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if the response cannot be sent.
161    pub async fn send(self) -> Result<(), NetworkError> {
162        use base64::Engine;
163
164        let body = self
165            .body
166            .map(|b| base64::engine::general_purpose::STANDARD.encode(&b));
167
168        let params = FulfillRequestParams {
169            request_id: self.route.request_id().to_string(),
170            response_code: i32::from(self.status),
171            response_headers: if self.headers.is_empty() {
172                None
173            } else {
174                Some(self.headers)
175            },
176            binary_response_headers: None,
177            body,
178            response_phrase: self.status_text,
179        };
180
181        self.route.send_fulfill(params).await
182    }
183}
184
185/// Builder for continuing a request with optional modifications.
186#[derive(Debug)]
187pub struct ContinueBuilder<'a> {
188    pub(super) route: &'a Route,
189    pub(super) url: Option<String>,
190    pub(super) method: Option<String>,
191    pub(super) headers: Vec<HeaderEntry>,
192    pub(super) post_data: Option<Vec<u8>>,
193}
194
195impl<'a> ContinueBuilder<'a> {
196    pub(super) fn new(route: &'a Route) -> Self {
197        Self {
198            route,
199            url: None,
200            method: None,
201            headers: Vec::new(),
202            post_data: None,
203        }
204    }
205
206    /// Override the request URL.
207    #[must_use]
208    pub fn url(mut self, url: impl Into<String>) -> Self {
209        self.url = Some(url.into());
210        self
211    }
212
213    /// Override the request method.
214    #[must_use]
215    pub fn method(mut self, method: impl Into<String>) -> Self {
216        self.method = Some(method.into());
217        self
218    }
219
220    /// Add or override a request header.
221    #[must_use]
222    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
223        self.headers.push(HeaderEntry {
224            name: name.into(),
225            value: value.into(),
226        });
227        self
228    }
229
230    /// Set multiple request headers.
231    #[must_use]
232    pub fn headers(
233        mut self,
234        headers: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
235    ) -> Self {
236        for (name, value) in headers {
237            self.headers.push(HeaderEntry {
238                name: name.into(),
239                value: value.into(),
240            });
241        }
242        self
243    }
244
245    /// Override the request POST data.
246    ///
247    /// The data will be base64-encoded for the CDP command.
248    #[must_use]
249    pub fn post_data(mut self, data: impl Into<Vec<u8>>) -> Self {
250        self.post_data = Some(data.into());
251        self
252    }
253
254    /// Continue the request (applying any modifications).
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if the request cannot be continued.
259    pub async fn send(self) -> Result<(), NetworkError> {
260        use base64::Engine;
261
262        let post_data = self
263            .post_data
264            .map(|d| base64::engine::general_purpose::STANDARD.encode(&d));
265
266        let params = ContinueRequestParams {
267            request_id: self.route.request_id().to_string(),
268            url: self.url,
269            method: self.method,
270            post_data,
271            headers: if self.headers.is_empty() {
272                None
273            } else {
274                Some(self.headers)
275            },
276            intercept_response: None,
277        };
278
279        self.route.send_continue(params).await
280    }
281}
282
283// Allow `route.continue_().await` without calling `.send()`
284impl ContinueBuilder<'_> {
285    /// Await the continue operation.
286    pub async fn await_continue(self) -> Result<(), NetworkError> {
287        self.send().await
288    }
289}
290
291impl<'a> std::future::IntoFuture for ContinueBuilder<'a> {
292    type Output = Result<(), NetworkError>;
293    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
294
295    fn into_future(self) -> Self::IntoFuture {
296        Box::pin(self.send())
297    }
298}
299
300// FetchBuilder and FetchedResponse are in route_fetch.rs